1.3.72
 
Loading...
Searching...
No Matches
Context_fileIO.cpp
Go to the documentation of this file.
1
16#include "Context.h"
17
18using namespace helios;
19
20// Geometric tolerance for triangle area validation
21// Triangles with area below this threshold are considered degenerate and skipped
22static constexpr float MIN_TRIANGLE_AREA_THRESHOLD = 1e-8f;
23
24int XMLparser::parse_data_float(const pugi::xml_node &node_data, std::vector<float> &data) {
25 std::string data_str = node_data.child_value();
26 data.resize(0);
27 if (!data_str.empty()) {
28 std::istringstream data_stream(data_str);
29 std::string tmp_s;
30 float tmp_f;
31 while (data_stream >> tmp_s) {
32 if (parse_float(tmp_s, tmp_f)) {
33 data.push_back(tmp_f);
34 } else {
35 return 2;
36 }
37 }
38 } else {
39 return 1;
40 }
41
42 return 0;
43}
44
45int XMLparser::parse_data_double(const pugi::xml_node &node_data, std::vector<double> &data) {
46 std::string data_str = node_data.child_value();
47 data.resize(0);
48 if (!data_str.empty()) {
49 std::istringstream data_stream(data_str);
50 std::string tmp_s;
51 double tmp_f;
52 while (data_stream >> tmp_s) {
53 if (parse_double(tmp_s, tmp_f)) {
54 data.push_back(tmp_f);
55 } else {
56 return 2;
57 }
58 }
59 } else {
60 return 1;
61 }
62
63 return 0;
64}
65
66int XMLparser::parse_data_int(const pugi::xml_node &node_data, std::vector<int> &data) {
67 std::string data_str = node_data.child_value();
68 data.resize(0);
69 if (!data_str.empty()) {
70 std::istringstream data_stream(data_str);
71 std::string tmp_s;
72 int tmp_f;
73 while (data_stream >> tmp_s) {
74 if (parse_int(tmp_s, tmp_f)) {
75 data.push_back(tmp_f);
76 } else {
77 return 2;
78 }
79 }
80 } else {
81 return 1;
82 }
83
84 return 0;
85}
86
87int XMLparser::parse_data_uint(const pugi::xml_node &node_data, std::vector<uint> &data) {
88 std::string data_str = node_data.child_value();
89 data.resize(0);
90 if (!data_str.empty()) {
91 std::istringstream data_stream(data_str);
92 std::string tmp_s;
93 uint tmp_f;
94 while (data_stream >> tmp_s) {
95 if (parse_uint(tmp_s, tmp_f)) {
96 data.push_back(tmp_f);
97 } else {
98 return 2;
99 }
100 }
101 } else {
102 return 1;
103 }
104
105 return 0;
106}
107
108int XMLparser::parse_data_string(const pugi::xml_node &node_data, std::vector<std::string> &data) {
109 std::string data_str = node_data.child_value();
110 data.resize(0);
111 if (!data_str.empty()) {
112 std::istringstream data_stream(data_str);
113 std::string tmp_s;
114 while (data_stream >> tmp_s) {
115 data.push_back(tmp_s);
116 }
117 } else {
118 return 1;
119 }
120
121 return 0;
122}
123
124int XMLparser::parse_data_vec2(const pugi::xml_node &node_data, std::vector<vec2> &data) {
125 std::string data_str = node_data.child_value();
126 data.resize(0);
127 if (!data_str.empty()) {
128 std::istringstream data_stream(data_str);
129 std::vector<std::string> tmp_s(2);
130 vec2 tmp;
131 while (data_stream >> tmp_s[0]) {
132 data_stream >> tmp_s[1];
133 if (!parse_float(tmp_s[0], tmp.x) || !parse_float(tmp_s[1], tmp.y)) {
134 return 2;
135 } else {
136 data.push_back(tmp);
137 }
138 }
139 } else {
140 return 1;
141 }
142
143 return 0;
144}
145
146int XMLparser::parse_data_vec3(const pugi::xml_node &node_data, std::vector<vec3> &data) {
147 std::string data_str = node_data.child_value();
148 data.resize(0);
149 if (!data_str.empty()) {
150 std::istringstream data_stream(data_str);
151 std::vector<std::string> tmp_s(3);
152 vec3 tmp;
153 while (data_stream >> tmp_s[0]) {
154 data_stream >> tmp_s[1];
155 data_stream >> tmp_s[2];
156 if (!parse_float(tmp_s[0], tmp.x) || !parse_float(tmp_s[1], tmp.y) || !parse_float(tmp_s[2], tmp.z)) {
157 return 2;
158 } else {
159 data.push_back(tmp);
160 }
161 }
162 } else {
163 return 1;
164 }
165
166 return 0;
167}
168
169int XMLparser::parse_data_vec4(const pugi::xml_node &node_data, std::vector<vec4> &data) {
170 std::string data_str = node_data.child_value();
171 data.resize(0);
172 if (!data_str.empty()) {
173 std::istringstream data_stream(data_str);
174 std::vector<std::string> tmp_s(4);
175 vec4 tmp;
176 while (data_stream >> tmp_s[0]) {
177 data_stream >> tmp_s[1];
178 data_stream >> tmp_s[2];
179 data_stream >> tmp_s[3];
180 if (!parse_float(tmp_s[0], tmp.x) || !parse_float(tmp_s[1], tmp.y) || !parse_float(tmp_s[2], tmp.z) || !parse_float(tmp_s[3], tmp.w)) {
181 return 2;
182 } else {
183 data.push_back(tmp);
184 }
185 }
186 } else {
187 return 1;
188 }
189
190 return 0;
191}
192
193int XMLparser::parse_data_int2(const pugi::xml_node &node_data, std::vector<int2> &data) {
194 std::string data_str = node_data.child_value();
195 data.resize(0);
196 if (!data_str.empty()) {
197 std::istringstream data_stream(data_str);
198 std::vector<std::string> tmp_s(2);
199 int2 tmp;
200 while (data_stream >> tmp_s[0]) {
201 data_stream >> tmp_s[1];
202 if (!parse_int(tmp_s[0], tmp.x) || !parse_int(tmp_s[1], tmp.y)) {
203 return 2;
204 } else {
205 data.push_back(tmp);
206 }
207 }
208 } else {
209 return 1;
210 }
211
212 return 0;
213}
214
215int XMLparser::parse_data_int3(const pugi::xml_node &node_data, std::vector<int3> &data) {
216 std::string data_str = node_data.child_value();
217 data.resize(0);
218 if (!data_str.empty()) {
219 std::istringstream data_stream(data_str);
220 std::vector<std::string> tmp_s(3);
221 int3 tmp;
222 while (data_stream >> tmp_s[0]) {
223 data_stream >> tmp_s[1];
224 data_stream >> tmp_s[2];
225 if (!parse_int(tmp_s[0], tmp.x) || !parse_int(tmp_s[1], tmp.y) || !parse_int(tmp_s[2], tmp.z)) {
226 return 2;
227 } else {
228 data.push_back(tmp);
229 }
230 }
231 } else {
232 return 1;
233 }
234
235 return 0;
236}
237
238int XMLparser::parse_data_int4(const pugi::xml_node &node_data, std::vector<int4> &data) {
239 std::string data_str = node_data.child_value();
240 data.resize(0);
241 if (!data_str.empty()) {
242 std::istringstream data_stream(data_str);
243 std::vector<std::string> tmp_s(4);
244 int4 tmp;
245 while (data_stream >> tmp_s[0]) {
246 data_stream >> tmp_s[1];
247 data_stream >> tmp_s[2];
248 data_stream >> tmp_s[3];
249 if (!parse_int(tmp_s[0], tmp.x) || !parse_int(tmp_s[1], tmp.y) || !parse_int(tmp_s[2], tmp.z) || !parse_int(tmp_s[3], tmp.w)) {
250 return 2;
251 } else {
252 data.push_back(tmp);
253 }
254 }
255 } else {
256 return 1;
257 }
258
259 return 0;
260}
261
262int XMLparser::parse_objID(const pugi::xml_node &node, uint &objID) {
263 pugi::xml_node objID_node = node.child("objID");
264 std::string oid = trim_whitespace(objID_node.child_value());
265 objID = 0;
266 if (!oid.empty()) {
267 if (!parse_uint(oid, objID)) {
268 return 2;
269 }
270 } else {
271 return 1;
272 }
273
274 return 0;
275}
276
277int XMLparser::parse_transform(const pugi::xml_node &node, float (&transform)[16]) {
278 pugi::xml_node transform_node = node.child("transform");
279
280 std::string transform_str = transform_node.child_value();
281 if (transform_str.empty()) {
282 makeIdentityMatrix(transform);
283 return 1;
284 } else {
285 std::istringstream stream(transform_str);
286 std::string tmp_s;
287 float tmp;
288 int i = 0;
289 while (stream >> tmp_s) {
290 if (parse_float(tmp_s, tmp)) {
291 transform[i] = tmp;
292 i++;
293 } else {
294 return 2;
295 }
296 }
297 if (i != 16) {
298 return 3;
299 }
300 }
301 return 0;
302}
303
304int XMLparser::parse_texture(const pugi::xml_node &node, std::string &texture) {
305 pugi::xml_node texture_node = node.child("texture");
306 std::string texfile = trim_whitespace(texture_node.child_value());
307 if (texfile.empty()) {
308 texture = "none";
309 return 1;
310 } else {
311 texture = texfile;
312 return 0;
313 }
314}
315
316int XMLparser::parse_textureUV(const pugi::xml_node &node, std::vector<vec2> &uvs) {
317 pugi::xml_node uv_node = node.child("textureUV");
318 std::string texUV = uv_node.child_value();
319 if (!texUV.empty()) {
320 std::istringstream uv_stream(texUV);
321 std::vector<std::string> tmp_s(2);
322 vec2 tmp;
323 while (uv_stream >> tmp_s[0]) {
324 uv_stream >> tmp_s[1];
325 if (!parse_float(tmp_s[0], tmp.x) || !parse_float(tmp_s[1], tmp.y)) {
326 return 2;
327 } else {
328 uvs.push_back(tmp);
329 }
330 }
331 } else {
332 return 1;
333 }
334
335 return 0;
336}
337
338int XMLparser::parse_solid_fraction(const pugi::xml_node &node, float &solid_fraction) {
339 pugi::xml_node sfrac_node = node.child("solid_fraction");
340 std::string sfrac = trim_whitespace(sfrac_node.child_value());
341 if (!sfrac.empty()) {
342 if (!parse_float(sfrac, solid_fraction)) {
343 return 2;
344 }
345 } else {
346 return 1;
347 }
348 return 0;
349}
350
351int XMLparser::parse_vertices(const pugi::xml_node &node, std::vector<float> &vertices) {
352 vertices.resize(12);
353
354 pugi::xml_node vertices_node = node.child("vertices");
355
356 std::string vertices_str = vertices_node.child_value();
357 if (!vertices_str.empty()) {
358 std::istringstream stream(vertices_str);
359 std::string tmp_s;
360 float tmp;
361 int i = 0;
362 while (stream >> tmp_s) {
363 if (i > 11) {
364 return 3;
365 } else if (parse_float(tmp_s, tmp)) {
366 vertices.at(i) = tmp;
367 i++;
368 } else {
369 return 2;
370 }
371 }
372 vertices.resize(i);
373 } else {
374 return 1;
375 }
376
377 return 0;
378}
379
380int XMLparser::parse_subdivisions(const pugi::xml_node &node, uint &subdivisions) {
381 pugi::xml_node subdiv_node = node.child("subdivisions");
382 std::string subdiv = trim_whitespace(subdiv_node.child_value());
383 if (!subdiv.empty()) {
384 if (!parse_uint(subdiv, subdivisions)) {
385 return 2;
386 }
387 } else {
388 return 1;
389 }
390 return 0;
391}
392
393int XMLparser::parse_subdivisions(const pugi::xml_node &node, int2 &subdivisions) {
394 pugi::xml_node subdiv_node = node.child("subdivisions");
395 std::string subdiv = trim_whitespace(subdiv_node.child_value());
396 if (!subdiv.empty()) {
397 std::istringstream data_stream(subdiv);
398 std::vector<std::string> tmp_s(2);
399 data_stream >> tmp_s[0];
400 data_stream >> tmp_s[1];
401 if (!parse_int(tmp_s[0], subdivisions.x) || !parse_int(tmp_s[1], subdivisions.y)) {
402 return 2;
403 }
404 } else {
405 return 1;
406 }
407 return 0;
408}
409
410int XMLparser::parse_subdivisions(const pugi::xml_node &node, int3 &subdivisions) {
411 pugi::xml_node subdiv_node = node.child("subdivisions");
412 std::string subdiv = trim_whitespace(subdiv_node.child_value());
413 if (!subdiv.empty()) {
414 std::istringstream data_stream(subdiv);
415 std::vector<std::string> tmp_s(3);
416 data_stream >> tmp_s[0];
417 data_stream >> tmp_s[1];
418 data_stream >> tmp_s[2];
419 if (!parse_int(tmp_s[0], subdivisions.x) || !parse_int(tmp_s[1], subdivisions.y) || !parse_int(tmp_s[2], subdivisions.z)) {
420 return 2;
421 }
422 } else {
423 return 1;
424 }
425 return 0;
426}
427
428int XMLparser::parse_nodes(const pugi::xml_node &node, std::vector<vec3> &nodes) {
429 pugi::xml_node node_data = node.child("nodes");
430 std::string data_str = node_data.child_value();
431 nodes.resize(0);
432 if (!data_str.empty()) {
433 std::istringstream data_stream(data_str);
434 std::vector<std::string> tmp_s(3);
435 vec3 tmp;
436 while (data_stream >> tmp_s[0]) {
437 data_stream >> tmp_s[1];
438 data_stream >> tmp_s[2];
439 if (!parse_float(tmp_s[0], tmp.x) || !parse_float(tmp_s[1], tmp.y) || !parse_float(tmp_s[2], tmp.z)) {
440 return 2;
441 } else {
442 nodes.push_back(tmp);
443 }
444 }
445 } else {
446 return 1;
447 }
448
449 return 0;
450}
451
452int XMLparser::parse_radius(const pugi::xml_node &node, std::vector<float> &radius) {
453 pugi::xml_node node_data = node.child("radius");
454 std::string data_str = node_data.child_value();
455 radius.resize(0);
456 if (!data_str.empty()) {
457 std::istringstream data_stream(data_str);
458 std::string tmp_s;
459 float tmp_f;
460 while (data_stream >> tmp_s) {
461 if (parse_float(tmp_s, tmp_f)) {
462 radius.push_back(tmp_f);
463 } else {
464 return 2;
465 }
466 }
467 } else {
468 return 1;
469 }
470
471 return 0;
472}
473
474void Context::loadMaterialData(pugi::xml_node mat_node, const std::string &material_label) {
475 // Load uint data
476 for (pugi::xml_node data = mat_node.child("data_uint"); data; data = data.next_sibling("data_uint")) {
477 const char *label = data.attribute("label").value();
478 std::vector<uint> datav;
479 if (XMLparser::parse_data_uint(data, datav) != 0 || datav.empty()) {
480 helios_runtime_error("ERROR (Context::loadXML): Material data tag <data_uint> with label " + std::string(label) + " contained invalid data.");
481 }
482 if (datav.size() == 1) {
483 setMaterialData(material_label, label, datav.front());
484 } else if (datav.size() > 1) {
485 setMaterialData(material_label, label, datav);
486 }
487 }
488
489 // Load int data
490 for (pugi::xml_node data = mat_node.child("data_int"); data; data = data.next_sibling("data_int")) {
491 const char *label = data.attribute("label").value();
492 std::vector<int> datav;
493 if (XMLparser::parse_data_int(data, datav) != 0 || datav.empty()) {
494 helios_runtime_error("ERROR (Context::loadXML): Material data tag <data_int> with label " + std::string(label) + " contained invalid data.");
495 }
496 if (datav.size() == 1) {
497 setMaterialData(material_label, label, datav.front());
498 } else if (datav.size() > 1) {
499 setMaterialData(material_label, label, datav);
500 }
501 }
502
503 // Load float data
504 for (pugi::xml_node data = mat_node.child("data_float"); data; data = data.next_sibling("data_float")) {
505 const char *label = data.attribute("label").value();
506 std::vector<float> datav;
507 if (XMLparser::parse_data_float(data, datav) != 0 || datav.empty()) {
508 helios_runtime_error("ERROR (Context::loadXML): Material data tag <data_float> with label " + std::string(label) + " contained invalid data.");
509 }
510 if (datav.size() == 1) {
511 setMaterialData(material_label, label, datav.front());
512 } else if (datav.size() > 1) {
513 setMaterialData(material_label, label, datav);
514 }
515 }
516
517 // Load double data
518 for (pugi::xml_node data = mat_node.child("data_double"); data; data = data.next_sibling("data_double")) {
519 const char *label = data.attribute("label").value();
520 std::vector<double> datav;
521 if (XMLparser::parse_data_double(data, datav) != 0 || datav.empty()) {
522 helios_runtime_error("ERROR (Context::loadXML): Material data tag <data_double> with label " + std::string(label) + " contained invalid data.");
523 }
524 if (datav.size() == 1) {
525 setMaterialData(material_label, label, datav.front());
526 } else if (datav.size() > 1) {
527 setMaterialData(material_label, label, datav);
528 }
529 }
530
531 // Load vec2 data
532 for (pugi::xml_node data = mat_node.child("data_vec2"); data; data = data.next_sibling("data_vec2")) {
533 const char *label = data.attribute("label").value();
534 std::vector<vec2> datav;
535 if (XMLparser::parse_data_vec2(data, datav) != 0 || datav.empty()) {
536 helios_runtime_error("ERROR (Context::loadXML): Material data tag <data_vec2> with label " + std::string(label) + " contained invalid data.");
537 }
538 if (datav.size() == 1) {
539 setMaterialData(material_label, label, datav.front());
540 } else if (datav.size() > 1) {
541 setMaterialData(material_label, label, datav);
542 }
543 }
544
545 // Load vec3 data
546 for (pugi::xml_node data = mat_node.child("data_vec3"); data; data = data.next_sibling("data_vec3")) {
547 const char *label = data.attribute("label").value();
548 std::vector<vec3> datav;
549 if (XMLparser::parse_data_vec3(data, datav) != 0 || datav.empty()) {
550 helios_runtime_error("ERROR (Context::loadXML): Material data tag <data_vec3> with label " + std::string(label) + " contained invalid data.");
551 }
552 if (datav.size() == 1) {
553 setMaterialData(material_label, label, datav.front());
554 } else if (datav.size() > 1) {
555 setMaterialData(material_label, label, datav);
556 }
557 }
558
559 // Load vec4 data
560 for (pugi::xml_node data = mat_node.child("data_vec4"); data; data = data.next_sibling("data_vec4")) {
561 const char *label = data.attribute("label").value();
562 std::vector<vec4> datav;
563 if (XMLparser::parse_data_vec4(data, datav) != 0 || datav.empty()) {
564 helios_runtime_error("ERROR (Context::loadXML): Material data tag <data_vec4> with label " + std::string(label) + " contained invalid data.");
565 }
566 if (datav.size() == 1) {
567 setMaterialData(material_label, label, datav.front());
568 } else if (datav.size() > 1) {
569 setMaterialData(material_label, label, datav);
570 }
571 }
572
573 // Load int2 data
574 for (pugi::xml_node data = mat_node.child("data_int2"); data; data = data.next_sibling("data_int2")) {
575 const char *label = data.attribute("label").value();
576 std::vector<int2> datav;
577 if (XMLparser::parse_data_int2(data, datav) != 0 || datav.empty()) {
578 helios_runtime_error("ERROR (Context::loadXML): Material data tag <data_int2> with label " + std::string(label) + " contained invalid data.");
579 }
580 if (datav.size() == 1) {
581 setMaterialData(material_label, label, datav.front());
582 } else if (datav.size() > 1) {
583 setMaterialData(material_label, label, datav);
584 }
585 }
586
587 // Load int3 data
588 for (pugi::xml_node data = mat_node.child("data_int3"); data; data = data.next_sibling("data_int3")) {
589 const char *label = data.attribute("label").value();
590 std::vector<int3> datav;
591 if (XMLparser::parse_data_int3(data, datav) != 0 || datav.empty()) {
592 helios_runtime_error("ERROR (Context::loadXML): Material data tag <data_int3> with label " + std::string(label) + " contained invalid data.");
593 }
594 if (datav.size() == 1) {
595 setMaterialData(material_label, label, datav.front());
596 } else if (datav.size() > 1) {
597 setMaterialData(material_label, label, datav);
598 }
599 }
600
601 // Load int4 data
602 for (pugi::xml_node data = mat_node.child("data_int4"); data; data = data.next_sibling("data_int4")) {
603 const char *label = data.attribute("label").value();
604 std::vector<int4> datav;
605 if (XMLparser::parse_data_int4(data, datav) != 0 || datav.empty()) {
606 helios_runtime_error("ERROR (Context::loadXML): Material data tag <data_int4> with label " + std::string(label) + " contained invalid data.");
607 }
608 if (datav.size() == 1) {
609 setMaterialData(material_label, label, datav.front());
610 } else if (datav.size() > 1) {
611 setMaterialData(material_label, label, datav);
612 }
613 }
614
615 // Load string data
616 for (pugi::xml_node data = mat_node.child("data_string"); data; data = data.next_sibling("data_string")) {
617 const char *label = data.attribute("label").value();
618 std::vector<std::string> datav;
619 if (XMLparser::parse_data_string(data, datav) != 0 || datav.empty()) {
620 helios_runtime_error("ERROR (Context::loadXML): Material data tag <data_string> with label " + std::string(label) + " contained invalid data.");
621 }
622 if (datav.size() == 1) {
623 setMaterialData(material_label, label, datav.front());
624 } else if (datav.size() > 1) {
625 setMaterialData(material_label, label, datav);
626 }
627 }
628}
629
630void Context::loadPData(pugi::xml_node p, uint UUID) {
631 for (pugi::xml_node data = p.child("data_int"); data; data = data.next_sibling("data_int")) {
632 const char *label = data.attribute("label").value();
633
634 std::vector<int> datav;
635 if (XMLparser::parse_data_int(data, datav) != 0 || datav.empty()) {
636 helios_runtime_error("ERROR (Context::loadXML): Primitive data tag <data_int> with label " + std::string(label) + " contained invalid data.");
637 }
638
639 if (datav.size() == 1) {
640 setPrimitiveData(UUID, label, datav.front());
641 } else if (datav.size() > 1) {
642 setPrimitiveData(UUID, label, datav);
643 }
644 }
645
646 for (pugi::xml_node data = p.child("data_uint"); data; data = data.next_sibling("data_uint")) {
647 const char *label = data.attribute("label").value();
648
649 std::vector<uint> datav;
650 if (XMLparser::parse_data_uint(data, datav) != 0 || datav.empty()) {
651 helios_runtime_error("ERROR (Context::loadXML): Primitive data tag <data_uint> with label " + std::string(label) + " contained invalid data.");
652 }
653
654 if (datav.size() == 1) {
655 setPrimitiveData(UUID, label, datav.front());
656 } else if (datav.size() > 1) {
657 setPrimitiveData(UUID, label, datav);
658 }
659 }
660
661 for (pugi::xml_node data = p.child("data_float"); data; data = data.next_sibling("data_float")) {
662 const char *label = data.attribute("label").value();
663
664 std::vector<float> datav;
665 if (XMLparser::parse_data_float(data, datav) != 0 || datav.empty()) {
666 helios_runtime_error("ERROR (Context::loadXML): Primitive data tag <data_float> with label " + std::string(label) + " contained invalid data.");
667 }
668
669 if (datav.size() == 1) {
670 setPrimitiveData(UUID, label, datav.front());
671 } else if (datav.size() > 1) {
672 setPrimitiveData(UUID, label, datav);
673 }
674 }
675
676 for (pugi::xml_node data = p.child("data_double"); data; data = data.next_sibling("data_double")) {
677 const char *label = data.attribute("label").value();
678
679 std::vector<double> datav;
680 if (XMLparser::parse_data_double(data, datav) != 0 || datav.empty()) {
681 helios_runtime_error("ERROR (Context::loadXML): Primitive data tag <data_double> with label " + std::string(label) + " contained invalid data.");
682 }
683
684 if (datav.size() == 1) {
685 setPrimitiveData(UUID, label, datav.front());
686 } else if (datav.size() > 1) {
687 setPrimitiveData(UUID, label, datav);
688 }
689 }
690
691 for (pugi::xml_node data = p.child("data_vec2"); data; data = data.next_sibling("data_vec2")) {
692 const char *label = data.attribute("label").value();
693
694 std::vector<vec2> datav;
695 if (XMLparser::parse_data_vec2(data, datav) != 0 || datav.empty()) {
696 helios_runtime_error("ERROR (Context::loadXML): Primitive data tag <data_vec2> with label " + std::string(label) + " contained invalid data.");
697 }
698
699 if (datav.size() == 1) {
700 setPrimitiveData(UUID, label, datav.front());
701 } else if (datav.size() > 1) {
702 setPrimitiveData(UUID, label, datav);
703 }
704 }
705
706 for (pugi::xml_node data = p.child("data_vec3"); data; data = data.next_sibling("data_vec3")) {
707 const char *label = data.attribute("label").value();
708
709 std::vector<vec3> datav;
710 if (XMLparser::parse_data_vec3(data, datav) != 0 || datav.empty()) {
711 helios_runtime_error("ERROR (Context::loadXML): Primitive data tag <data_vec3> with label " + std::string(label) + " contained invalid data.");
712 }
713
714 if (datav.size() == 1) {
715 setPrimitiveData(UUID, label, datav.front());
716 } else if (datav.size() > 1) {
717 setPrimitiveData(UUID, label, datav);
718 }
719 }
720
721 for (pugi::xml_node data = p.child("data_vec4"); data; data = data.next_sibling("data_vec4")) {
722 const char *label = data.attribute("label").value();
723
724 std::vector<vec4> datav;
725 if (XMLparser::parse_data_vec4(data, datav) != 0 || datav.empty()) {
726 helios_runtime_error("ERROR (Context::loadXML): Primitive data tag <data_vec4> with label " + std::string(label) + " contained invalid data.");
727 }
728
729 if (datav.size() == 1) {
730 setPrimitiveData(UUID, label, datav.front());
731 } else if (datav.size() > 1) {
732 setPrimitiveData(UUID, label, datav);
733 }
734 }
735
736 for (pugi::xml_node data = p.child("data_int2"); data; data = data.next_sibling("data_int2")) {
737 const char *label = data.attribute("label").value();
738
739 std::vector<int2> datav;
740 if (XMLparser::parse_data_int2(data, datav) != 0 || datav.empty()) {
741 helios_runtime_error("ERROR (Context::loadXML): Primitive data tag <data_int2> with label " + std::string(label) + " contained invalid data.");
742 }
743
744 if (datav.size() == 1) {
745 setPrimitiveData(UUID, label, datav.front());
746 } else if (datav.size() > 1) {
747 setPrimitiveData(UUID, label, datav);
748 }
749 }
750
751 for (pugi::xml_node data = p.child("data_int3"); data; data = data.next_sibling("data_int3")) {
752 const char *label = data.attribute("label").value();
753
754 std::vector<int3> datav;
755 if (XMLparser::parse_data_int3(data, datav) != 0 || datav.empty()) {
756 helios_runtime_error("ERROR (Context::loadXML): Primitive data tag <data_int3> with label " + std::string(label) + " contained invalid data.");
757 }
758
759 if (datav.size() == 1) {
760 setPrimitiveData(UUID, label, datav.front());
761 } else if (datav.size() > 1) {
762 setPrimitiveData(UUID, label, datav);
763 }
764 }
765
766 for (pugi::xml_node data = p.child("data_int4"); data; data = data.next_sibling("data_int4")) {
767 const char *label = data.attribute("label").value();
768
769 std::vector<int4> datav;
770 if (XMLparser::parse_data_int4(data, datav) != 0 || datav.empty()) {
771 helios_runtime_error("ERROR (Context::loadXML): Primitive data tag <data_int4> with label " + std::string(label) + " contained invalid data.");
772 }
773
774 if (datav.size() == 1) {
775 setPrimitiveData(UUID, label, datav.front());
776 } else if (datav.size() > 1) {
777 setPrimitiveData(UUID, label, datav);
778 }
779 }
780
781 for (pugi::xml_node data = p.child("data_string"); data; data = data.next_sibling("data_string")) {
782 const char *label = data.attribute("label").value();
783
784 std::vector<std::string> datav;
785 if (XMLparser::parse_data_string(data, datav) != 0 || datav.empty()) {
786 helios_runtime_error("ERROR (Context::loadXML): Primitive data tag <data_string> with label " + std::string(label) + " contained invalid data.");
787 }
788
789 if (datav.size() == 1) {
790 setPrimitiveData(UUID, label, datav.front());
791 } else if (datav.size() > 1) {
792 setPrimitiveData(UUID, label, datav);
793 }
794 }
795}
796
797void Context::loadOData(pugi::xml_node p, uint ID) {
798 assert(doesObjectExist(ID));
799
800 for (pugi::xml_node data = p.child("data_int"); data; data = data.next_sibling("data_int")) {
801 const char *label = data.attribute("label").value();
802
803 std::vector<int> datav;
804 if (XMLparser::parse_data_int(data, datav) != 0 || datav.empty()) {
805 helios_runtime_error("ERROR (Context::loadXML): Object data tag <data_int> with label " + std::string(label) + " contained invalid data.");
806 }
807
808 if (datav.size() == 1) {
809 setObjectData(ID, label, datav.front());
810 } else if (datav.size() > 1) {
811 setObjectData(ID, label, datav);
812 }
813 }
814
815 for (pugi::xml_node data = p.child("data_uint"); data; data = data.next_sibling("data_uint")) {
816 const char *label = data.attribute("label").value();
817
818 std::vector<uint> datav;
819 if (XMLparser::parse_data_uint(data, datav) != 0 || datav.empty()) {
820 helios_runtime_error("ERROR (Context::loadXML): Object data tag <data_uint> with label " + std::string(label) + " contained invalid data.");
821 }
822
823 if (datav.size() == 1) {
824 setObjectData(ID, label, datav.front());
825 } else if (datav.size() > 1) {
826 setObjectData(ID, label, datav);
827 }
828 }
829
830 for (pugi::xml_node data = p.child("data_float"); data; data = data.next_sibling("data_float")) {
831 const char *label = data.attribute("label").value();
832
833 std::vector<float> datav;
834 if (XMLparser::parse_data_float(data, datav) != 0 || datav.empty()) {
835 helios_runtime_error("ERROR (Context::loadXML): Object data tag <data_float> with label " + std::string(label) + " contained invalid data.");
836 }
837
838 if (datav.size() == 1) {
839 setObjectData(ID, label, datav.front());
840 } else if (datav.size() > 1) {
841 setObjectData(ID, label, datav);
842 }
843 }
844
845 for (pugi::xml_node data = p.child("data_double"); data; data = data.next_sibling("data_double")) {
846 const char *label = data.attribute("label").value();
847
848 std::vector<double> datav;
849 if (XMLparser::parse_data_double(data, datav) != 0 || datav.empty()) {
850 helios_runtime_error("ERROR (Context::loadXML): Object data tag <data_double> with label " + std::string(label) + " contained invalid data.");
851 }
852
853 if (datav.size() == 1) {
854 setObjectData(ID, label, datav.front());
855 } else if (datav.size() > 1) {
856 setObjectData(ID, label, datav);
857 }
858 }
859
860 for (pugi::xml_node data = p.child("data_vec2"); data; data = data.next_sibling("data_vec2")) {
861 const char *label = data.attribute("label").value();
862
863 std::vector<vec2> datav;
864 if (XMLparser::parse_data_vec2(data, datav) != 0 || datav.empty()) {
865 helios_runtime_error("ERROR (Context::loadXML): Object data tag <data_vec2> with label " + std::string(label) + " contained invalid data.");
866 }
867
868 if (datav.size() == 1) {
869 setObjectData(ID, label, datav.front());
870 } else if (datav.size() > 1) {
871 setObjectData(ID, label, datav);
872 }
873 }
874
875 for (pugi::xml_node data = p.child("data_vec3"); data; data = data.next_sibling("data_vec3")) {
876 const char *label = data.attribute("label").value();
877
878 std::vector<vec3> datav;
879 if (XMLparser::parse_data_vec3(data, datav) != 0 || datav.empty()) {
880 helios_runtime_error("ERROR (Context::loadXML): Object data tag <data_vec3> with label " + std::string(label) + " contained invalid data.");
881 }
882
883 if (datav.size() == 1) {
884 setObjectData(ID, label, datav.front());
885 } else if (datav.size() > 1) {
886 setObjectData(ID, label, datav);
887 }
888 }
889
890 for (pugi::xml_node data = p.child("data_vec4"); data; data = data.next_sibling("data_vec4")) {
891 const char *label = data.attribute("label").value();
892
893 std::vector<vec4> datav;
894 if (XMLparser::parse_data_vec4(data, datav) != 0 || datav.empty()) {
895 helios_runtime_error("ERROR (Context::loadXML): Object data tag <data_vec4> with label " + std::string(label) + " contained invalid data.");
896 }
897
898 if (datav.size() == 1) {
899 setObjectData(ID, label, datav.front());
900 } else if (datav.size() > 1) {
901 setObjectData(ID, label, datav);
902 }
903 }
904
905 for (pugi::xml_node data = p.child("data_int2"); data; data = data.next_sibling("data_int2")) {
906 const char *label = data.attribute("label").value();
907
908 std::vector<int2> datav;
909 if (XMLparser::parse_data_int2(data, datav) != 0 || datav.empty()) {
910 helios_runtime_error("ERROR (Context::loadXML): Object data tag <data_int2> with label " + std::string(label) + " contained invalid data.");
911 }
912
913 if (datav.size() == 1) {
914 setObjectData(ID, label, datav.front());
915 } else if (datav.size() > 1) {
916 setObjectData(ID, label, datav);
917 }
918 }
919
920 for (pugi::xml_node data = p.child("data_int3"); data; data = data.next_sibling("data_int3")) {
921 const char *label = data.attribute("label").value();
922
923 std::vector<int3> datav;
924 if (XMLparser::parse_data_int3(data, datav) != 0 || datav.empty()) {
925 helios_runtime_error("ERROR (Context::loadXML): Object data tag <data_int3> with label " + std::string(label) + " contained invalid data.");
926 }
927
928 if (datav.size() == 1) {
929 setObjectData(ID, label, datav.front());
930 } else if (datav.size() > 1) {
931 setObjectData(ID, label, datav);
932 }
933 }
934
935 for (pugi::xml_node data = p.child("data_int4"); data; data = data.next_sibling("data_int4")) {
936 const char *label = data.attribute("label").value();
937
938 std::vector<int4> datav;
939 if (XMLparser::parse_data_int4(data, datav) != 0 || datav.empty()) {
940 helios_runtime_error("ERROR (Context::loadXML): Object data tag <data_int4> with label " + std::string(label) + " contained invalid data.");
941 }
942
943 if (datav.size() == 1) {
944 setObjectData(ID, label, datav.front());
945 } else if (datav.size() > 1) {
946 setObjectData(ID, label, datav);
947 }
948 }
949
950 for (pugi::xml_node data = p.child("data_string"); data; data = data.next_sibling("data_string")) {
951 const char *label = data.attribute("label").value();
952
953 std::vector<std::string> datav;
954 if (XMLparser::parse_data_string(data, datav) != 0 || datav.empty()) {
955 helios_runtime_error("ERROR (Context::loadXML): Object data tag <data_string> with label " + std::string(label) + " contained invalid data.");
956 }
957
958 if (datav.size() == 1) {
959 setObjectData(ID, label, datav.front());
960 } else if (datav.size() > 1) {
961 setObjectData(ID, label, datav);
962 }
963 }
964}
965
966void Context::loadOsubPData(pugi::xml_node p, uint ID) {
967 assert(doesObjectExist(ID));
968
969 std::vector<uint> prim_UUIDs = getObjectPointer_private(ID)->getPrimitiveUUIDs();
970
971 int u;
972
973 for (pugi::xml_node prim_data = p.child("primitive_data_int"); prim_data; prim_data = prim_data.next_sibling("primitive_data_int")) {
974 const char *label = prim_data.attribute("label").value();
975
976 u = 0;
977 for (pugi::xml_node data = prim_data.child("data"); data; data = data.next_sibling("data")) {
978 if (u >= prim_UUIDs.size()) {
979 std::cerr << "WARNING (Context::loadXML): There was a problem with reading object primitive data \"" << label
980 << "\". The number of data values provided does not match the number of primitives contained in this object. Skipping remaining data values." << std::endl;
981 break;
982 }
983
984 std::vector<int> datav;
985 if (XMLparser::parse_data_int(data, datav) != 0 || datav.empty()) {
986 helios_runtime_error("ERROR (Context::loadXML): Object member primitive data tag <primitive_data_int> with label " + std::string(label) + " contained invalid data.");
987 }
988
989 if (doesPrimitiveExist(prim_UUIDs.at(u))) {
990 if (datav.size() == 1) {
991 setPrimitiveData(prim_UUIDs.at(u), label, datav.front());
992 } else if (datav.size() > 1) {
993 setPrimitiveData(prim_UUIDs.at(u), label, datav);
994 }
995 }
996 u++;
997 }
998 }
999
1000 for (pugi::xml_node prim_data = p.child("primitive_data_uint"); prim_data; prim_data = prim_data.next_sibling("primitive_data_uint")) {
1001 const char *label = prim_data.attribute("label").value();
1002
1003 u = 0;
1004 for (pugi::xml_node data = prim_data.child("data"); data; data = data.next_sibling("data")) {
1005 if (u >= prim_UUIDs.size()) {
1006 std::cerr << "WARNING (Context::loadXML): There was a problem with reading object primitive data \"" << label
1007 << "\". The number of data values provided does not match the number of primitives contained in this object. Skipping remaining data values." << std::endl;
1008 break;
1009 }
1010
1011 std::vector<uint> datav;
1012 if (XMLparser::parse_data_uint(data, datav) != 0 || datav.empty()) {
1013 helios_runtime_error("ERROR (Context::loadXML): Object member primitive data tag <primitive_data_uint> with label " + std::string(label) + " contained invalid data.");
1014 }
1015
1016 if (doesPrimitiveExist(prim_UUIDs.at(u))) {
1017 if (datav.size() == 1) {
1018 setPrimitiveData(prim_UUIDs.at(u), label, datav.front());
1019 } else if (datav.size() > 1) {
1020 setPrimitiveData(prim_UUIDs.at(u), label, datav);
1021 }
1022 }
1023 u++;
1024 }
1025 }
1026
1027 for (pugi::xml_node prim_data = p.child("primitive_data_float"); prim_data; prim_data = prim_data.next_sibling("primitive_data_float")) {
1028 const char *label = prim_data.attribute("label").value();
1029
1030 u = 0;
1031 for (pugi::xml_node data = prim_data.child("data"); data; data = data.next_sibling("data")) {
1032 if (u >= prim_UUIDs.size()) {
1033 std::cerr << "WARNING (Context::loadXML): There was a problem with reading object primitive data \"" << label
1034 << "\". The number of data values provided does not match the number of primitives contained in this object. Skipping remaining data values." << std::endl;
1035 break;
1036 }
1037
1038 std::vector<float> datav;
1039 if (XMLparser::parse_data_float(data, datav) != 0 || datav.empty()) {
1040 helios_runtime_error("ERROR (Context::loadXML): Object member primitive data tag <primitive_data_float> with label " + std::string(label) + " contained invalid data.");
1041 }
1042
1043 if (doesPrimitiveExist(prim_UUIDs.at(u))) {
1044 if (datav.size() == 1) {
1045 setPrimitiveData(prim_UUIDs.at(u), label, datav.front());
1046 } else if (datav.size() > 1) {
1047 setPrimitiveData(prim_UUIDs.at(u), label, datav);
1048 }
1049 }
1050 u++;
1051 }
1052 }
1053
1054 for (pugi::xml_node prim_data = p.child("primitive_data_double"); prim_data; prim_data = prim_data.next_sibling("primitive_data_double")) {
1055 const char *label = prim_data.attribute("label").value();
1056
1057 u = 0;
1058 for (pugi::xml_node data = prim_data.child("data"); data; data = data.next_sibling("data")) {
1059 if (u >= prim_UUIDs.size()) {
1060 std::cerr << "WARNING (Context::loadXML): There was a problem with reading object primitive data \"" << label
1061 << "\". The number of data values provided does not match the number of primitives contained in this object. Skipping remaining data values." << std::endl;
1062 break;
1063 }
1064
1065 std::vector<double> datav;
1066 if (XMLparser::parse_data_double(data, datav) != 0 || datav.empty()) {
1067 helios_runtime_error("ERROR (Context::loadXML): Object member primitive data tag <primitive_data_double> with label " + std::string(label) + " contained invalid data.");
1068 }
1069
1070 if (doesPrimitiveExist(prim_UUIDs.at(u))) {
1071 if (datav.size() == 1) {
1072 setPrimitiveData(prim_UUIDs.at(u), label, datav.front());
1073 } else if (datav.size() > 1) {
1074 setPrimitiveData(prim_UUIDs.at(u), label, datav);
1075 }
1076 }
1077 u++;
1078 }
1079 }
1080
1081 for (pugi::xml_node prim_data = p.child("primitive_data_vec2"); prim_data; prim_data = prim_data.next_sibling("primitive_data_vec2")) {
1082 const char *label = prim_data.attribute("label").value();
1083
1084 u = 0;
1085 for (pugi::xml_node data = prim_data.child("data"); data; data = data.next_sibling("data")) {
1086 if (u >= prim_UUIDs.size()) {
1087 std::cerr << "WARNING (Context::loadXML): There was a problem with reading object primitive data \"" << label
1088 << "\". The number of data values provided does not match the number of primitives contained in this object. Skipping remaining data values." << std::endl;
1089 break;
1090 }
1091
1092 std::vector<vec2> datav;
1093 if (XMLparser::parse_data_vec2(data, datav) != 0 || datav.empty()) {
1094 helios_runtime_error("ERROR (Context::loadXML): Object member primitive data tag <primitive_data_vec2> with label " + std::string(label) + " contained invalid data.");
1095 }
1096
1097 if (doesPrimitiveExist(prim_UUIDs.at(u))) {
1098 if (datav.size() == 1) {
1099 setPrimitiveData(prim_UUIDs.at(u), label, datav.front());
1100 } else if (datav.size() > 1) {
1101 setPrimitiveData(prim_UUIDs.at(u), label, datav);
1102 }
1103 }
1104 u++;
1105 }
1106 }
1107
1108 for (pugi::xml_node prim_data = p.child("primitive_data_vec3"); prim_data; prim_data = prim_data.next_sibling("primitive_data_vec3")) {
1109 const char *label = prim_data.attribute("label").value();
1110
1111 u = 0;
1112 for (pugi::xml_node data = prim_data.child("data"); data; data = data.next_sibling("data")) {
1113 if (u >= prim_UUIDs.size()) {
1114 std::cerr << "WARNING (Context::loadXML): There was a problem with reading object primitive data \"" << label
1115 << "\". The number of data values provided does not match the number of primitives contained in this object. Skipping remaining data values." << std::endl;
1116 break;
1117 }
1118
1119 std::vector<vec3> datav;
1120 if (XMLparser::parse_data_vec3(data, datav) != 0 || datav.empty()) {
1121 helios_runtime_error("ERROR (Context::loadXML): Object member primitive data tag <primitive_data_vec3> with label " + std::string(label) + " contained invalid data.");
1122 }
1123
1124 if (doesPrimitiveExist(prim_UUIDs.at(u))) {
1125 if (datav.size() == 1) {
1126 setPrimitiveData(prim_UUIDs.at(u), label, datav.front());
1127 } else if (datav.size() > 1) {
1128 setPrimitiveData(prim_UUIDs.at(u), label, datav);
1129 }
1130 }
1131 u++;
1132 }
1133 }
1134
1135 for (pugi::xml_node prim_data = p.child("primitive_data_vec4"); prim_data; prim_data = prim_data.next_sibling("primitive_data_vec4")) {
1136 const char *label = prim_data.attribute("label").value();
1137
1138 u = 0;
1139 for (pugi::xml_node data = prim_data.child("data"); data; data = data.next_sibling("data")) {
1140 if (u >= prim_UUIDs.size()) {
1141 std::cerr << "WARNING (Context::loadXML): There was a problem with reading object primitive data \"" << label
1142 << "\". The number of data values provided does not match the number of primitives contained in this object. Skipping remaining data values." << std::endl;
1143 break;
1144 }
1145
1146 std::vector<vec4> datav;
1147 if (XMLparser::parse_data_vec4(data, datav) != 0 || datav.empty()) {
1148 helios_runtime_error("ERROR (Context::loadXML): Object member primitive data tag <primitive_data_vec4> with label " + std::string(label) + " contained invalid data.");
1149 }
1150
1151 if (doesPrimitiveExist(prim_UUIDs.at(u))) {
1152 if (datav.size() == 1) {
1153 setPrimitiveData(prim_UUIDs.at(u), label, datav.front());
1154 } else if (datav.size() > 1) {
1155 setPrimitiveData(prim_UUIDs.at(u), label, datav);
1156 }
1157 }
1158 u++;
1159 }
1160 }
1161
1162 for (pugi::xml_node prim_data = p.child("primitive_data_int2"); prim_data; prim_data = prim_data.next_sibling("primitive_data_int2")) {
1163 const char *label = prim_data.attribute("label").value();
1164
1165 u = 0;
1166 for (pugi::xml_node data = prim_data.child("data"); data; data = data.next_sibling("data")) {
1167 if (u >= prim_UUIDs.size()) {
1168 std::cerr << "WARNING (Context::loadXML): There was a problem with reading object primitive data \"" << label
1169 << "\". The number of data values provided does not match the number of primitives contained in this object. Skipping remaining data values." << std::endl;
1170 break;
1171 }
1172
1173 std::vector<int2> datav;
1174 if (XMLparser::parse_data_int2(data, datav) != 0 || datav.empty()) {
1175 helios_runtime_error("ERROR (Context::loadXML): Object member primitive data tag <primitive_data_int2> with label " + std::string(label) + " contained invalid data.");
1176 }
1177
1178 if (doesPrimitiveExist(prim_UUIDs.at(u))) {
1179 if (datav.size() == 1) {
1180 setPrimitiveData(prim_UUIDs.at(u), label, datav.front());
1181 } else if (datav.size() > 1) {
1182 setPrimitiveData(prim_UUIDs.at(u), label, datav);
1183 }
1184 }
1185 u++;
1186 }
1187 }
1188
1189 for (pugi::xml_node prim_data = p.child("primitive_data_int3"); prim_data; prim_data = prim_data.next_sibling("primitive_data_int3")) {
1190 const char *label = prim_data.attribute("label").value();
1191
1192 u = 0;
1193 for (pugi::xml_node data = prim_data.child("data"); data; data = data.next_sibling("data")) {
1194 if (u >= prim_UUIDs.size()) {
1195 std::cerr << "WARNING (Context::loadXML): There was a problem with reading object primitive data \"" << label
1196 << "\". The number of data values provided does not match the number of primitives contained in this object. Skipping remaining data values." << std::endl;
1197 break;
1198 }
1199
1200 std::vector<int3> datav;
1201 if (XMLparser::parse_data_int3(data, datav) != 0 || datav.empty()) {
1202 helios_runtime_error("ERROR (Context::loadXML): Object member primitive data tag <primitive_data_int3> with label " + std::string(label) + " contained invalid data.");
1203 }
1204
1205 if (doesPrimitiveExist(prim_UUIDs.at(u))) {
1206 if (datav.size() == 1) {
1207 setPrimitiveData(prim_UUIDs.at(u), label, datav.front());
1208 } else if (datav.size() > 1) {
1209 setPrimitiveData(prim_UUIDs.at(u), label, datav);
1210 }
1211 }
1212 u++;
1213 }
1214 }
1215
1216 for (pugi::xml_node prim_data = p.child("primitive_data_int4"); prim_data; prim_data = prim_data.next_sibling("primitive_data_int4")) {
1217 const char *label = prim_data.attribute("label").value();
1218
1219 u = 0;
1220 for (pugi::xml_node data = prim_data.child("data"); data; data = data.next_sibling("data")) {
1221 if (u >= prim_UUIDs.size()) {
1222 std::cerr << "WARNING (Context::loadXML): There was a problem with reading object primitive data \"" << label
1223 << "\". The number of data values provided does not match the number of primitives contained in this object. Skipping remaining data values." << std::endl;
1224 break;
1225 }
1226
1227 std::vector<int4> datav;
1228 if (XMLparser::parse_data_int4(data, datav) != 0 || datav.empty()) {
1229 helios_runtime_error("ERROR (Context::loadXML): Object member primitive data tag <primitive_data_int4> with label " + std::string(label) + " contained invalid data.");
1230 }
1231
1232 if (doesPrimitiveExist(prim_UUIDs.at(u))) {
1233 if (datav.size() == 1) {
1234 setPrimitiveData(prim_UUIDs.at(u), label, datav.front());
1235 } else if (datav.size() > 1) {
1236 setPrimitiveData(prim_UUIDs.at(u), label, datav);
1237 }
1238 }
1239 u++;
1240 }
1241 }
1242
1243 for (pugi::xml_node prim_data = p.child("primitive_data_string"); prim_data; prim_data = prim_data.next_sibling("primitive_data_string")) {
1244 const char *label = prim_data.attribute("label").value();
1245
1246 u = 0;
1247 for (pugi::xml_node data = prim_data.child("data"); data; data = data.next_sibling("data")) {
1248 if (u >= prim_UUIDs.size()) {
1249 std::cerr << "WARNING (Context::loadXML): There was a problem with reading object primitive data \"" << label
1250 << "\". The number of data values provided does not match the number of primitives contained in this object. Skipping remaining data values." << std::endl;
1251 break;
1252 }
1253
1254 std::vector<std::string> datav;
1255 if (XMLparser::parse_data_string(data, datav) != 0 || datav.empty()) {
1256 helios_runtime_error("ERROR (Context::loadXML): Object member primitive data tag <primitive_data_string> with label " + std::string(label) + " contained invalid data.");
1257 }
1258
1259 if (doesPrimitiveExist(prim_UUIDs.at(u))) {
1260 if (datav.size() == 1) {
1261 setPrimitiveData(prim_UUIDs.at(u), label, datav.front());
1262 } else if (datav.size() > 1) {
1263 setPrimitiveData(prim_UUIDs.at(u), label, datav);
1264 }
1265 }
1266 u++;
1267 }
1268 }
1269}
1270
1271std::vector<uint> Context::loadXML(const char *filename, bool quiet) {
1272 if (!quiet) {
1273 std::cout << "Loading XML file: " << filename << "..." << std::flush;
1274 }
1275
1276 std::string fn = filename;
1277 std::string ext = getFileExtension(filename);
1278 if (ext != ".xml" && ext != ".XML") {
1279 helios_runtime_error("failed.\n File " + fn + " is not XML format.");
1280 }
1281
1282 // Resolve file path using unified resolution
1283 std::filesystem::path resolved_path = resolveFilePath(filename);
1284 std::string resolved_filename = resolved_path.string();
1285
1286 XMLfiles.emplace_back(resolved_filename);
1287
1288 uint ID;
1289 std::vector<uint> UUID;
1290
1291 // Using "pugixml" parser. See pugixml.org
1292 pugi::xml_document xmldoc;
1293
1294 // load file
1295 pugi::xml_parse_result load_result = xmldoc.load_file(resolved_filename.c_str());
1296
1297 // error checking
1298 if (!load_result) {
1299 helios_runtime_error("failed.\n XML [" + std::string(filename) + "] parsed with errors, attr value: [" + xmldoc.child("node").attribute("attr").value() + "]\nError description: " + load_result.description() +
1300 "\nError offset: " + std::to_string(load_result.offset) + " (error at [..." + (filename + load_result.offset) + "]\n");
1301 }
1302
1303 pugi::xml_node helios = xmldoc.child("helios");
1304
1305 if (helios.empty()) {
1306 if (!quiet) {
1307 std::cout << "failed." << std::endl;
1308 }
1309 helios_runtime_error("ERROR (Context::loadXML): XML file must have tag '<helios> ... </helios>' bounding all other tags.");
1310 }
1311
1312 // if primitives are added that belong to an object, store their UUIDs here so that we can make sure their UUIDs are consistent
1313 std::map<uint, std::vector<uint>> object_prim_UUIDs;
1314
1315 //-------------- TIME/DATE ---------------//
1316
1317 for (pugi::xml_node p = helios.child("date"); p; p = p.next_sibling("date")) {
1318 pugi::xml_node year_node = p.child("year");
1319 const char *year_str = year_node.child_value();
1320 int year;
1321 if (!parse_int(year_str, year)) {
1322 helios_runtime_error("ERROR (Context::loadXML): Year given in 'date' block must be an integer value.");
1323 }
1324
1325 pugi::xml_node month_node = p.child("month");
1326 const char *month_str = month_node.child_value();
1327 int month;
1328 if (!parse_int(month_str, month)) {
1329 helios_runtime_error("ERROR (Context::loadXML): Month given in 'date' block must be an integer value.");
1330 }
1331
1332 pugi::xml_node day_node = p.child("day");
1333 const char *day_str = day_node.child_value();
1334 int day;
1335 if (!parse_int(day_str, day)) {
1336 helios_runtime_error("ERROR (Context::loadXML): Day given in 'date' block must be an integer value.");
1337 }
1338
1339 setDate(day, month, year);
1340 }
1341
1342 for (pugi::xml_node p = helios.child("time"); p; p = p.next_sibling("time")) {
1343 pugi::xml_node hour_node = p.child("hour");
1344 const char *hour_str = hour_node.child_value();
1345 int hour;
1346 if (!parse_int(hour_str, hour)) {
1347 helios_runtime_error("ERROR (Context::loadXML): Hour given in 'time' block must be an integer value.");
1348 }
1349
1350 pugi::xml_node minute_node = p.child("minute");
1351 const char *minute_str = minute_node.child_value();
1352 int minute;
1353 if (!parse_int(minute_str, minute)) {
1354 helios_runtime_error("ERROR (Context::loadXML): Minute given in 'time' block must be an integer value.");
1355 }
1356
1357 pugi::xml_node second_node = p.child("second");
1358 const char *second_str = second_node.child_value();
1359 int second;
1360 if (!parse_int(second_str, second)) {
1361 helios_runtime_error("ERROR (Context::loadXML): Second given in 'time' block must be an integer value.");
1362 }
1363
1364 setTime(second, minute, hour);
1365 }
1366
1367 //-------------- MATERIALS ---------------//
1368 // Map to track legacy numeric material IDs to labels for backward compatibility
1369 std::map<uint, std::string> legacy_material_id_to_label;
1370
1371 for (pugi::xml_node m = helios.child("materials"); m; m = m.next_sibling("materials")) {
1372 for (pugi::xml_node mat = m.child("material"); mat; mat = mat.next_sibling("material")) {
1373 std::string material_label;
1374 RGBAcolor color = make_RGBAcolor(0, 0, 0, 1);
1375 std::string texture_file;
1376 bool texture_override = false;
1377
1378 // Check for v3 format (label="...") first
1379 pugi::xml_attribute label_attr = mat.attribute("label");
1380 if (!label_attr.empty()) {
1381 material_label = label_attr.value();
1382 } else {
1383 // Check for v2 format (id="N")
1384 pugi::xml_attribute id_attr = mat.attribute("id");
1385 if (!id_attr.empty()) {
1386 uint matID = 0;
1387 const char *id_str = id_attr.value();
1388 if (!parse_uint(id_str, matID)) {
1389 helios_runtime_error("ERROR (Context::loadXML): Material ID must be an unsigned integer value.");
1390 }
1391 // Generate label from numeric ID for backward compatibility
1392 material_label = "__auto_material_" + std::to_string(matID);
1393 legacy_material_id_to_label[matID] = material_label;
1394 } else {
1395 helios_runtime_error("ERROR (Context::loadXML): Material must have either a 'label' or 'id' attribute.");
1396 }
1397 }
1398
1399 // Color
1400 pugi::xml_node color_node = mat.child("color");
1401 if (!color_node.empty()) {
1402 const char *color_str = color_node.child_value();
1403 std::istringstream color_stream(color_str);
1404 std::vector<float> color_vec;
1405 float tmp;
1406 while (color_stream >> tmp) {
1407 color_vec.push_back(tmp);
1408 }
1409 if (color_vec.size() == 3) {
1410 color = make_RGBAcolor(color_vec.at(0), color_vec.at(1), color_vec.at(2), 1.f);
1411 } else if (color_vec.size() == 4) {
1412 color = make_RGBAcolor(color_vec.at(0), color_vec.at(1), color_vec.at(2), color_vec.at(3));
1413 }
1414 }
1415
1416 // Texture
1417 pugi::xml_node texture_node = mat.child("texture");
1418 if (!texture_node.empty()) {
1419 texture_file = deblank(texture_node.child_value());
1420 if (!texture_file.empty()) {
1421 addTexture(texture_file.c_str());
1422 }
1423 }
1424
1425 // Texture override
1426 pugi::xml_node override_node = mat.child("texture_override");
1427 if (!override_node.empty()) {
1428 const char *override_str = override_node.child_value();
1429 int override_val;
1430 if (parse_int(override_str, override_val)) {
1431 texture_override = (override_val != 0);
1432 }
1433 }
1434
1435 // Twosided flag
1436 uint twosided = 1; // default: two-sided
1437 pugi::xml_node twosided_node = mat.child("twosided_flag");
1438 if (!twosided_node.empty()) {
1439 const char *twosided_str = twosided_node.child_value();
1440 int twosided_val;
1441 if (parse_int(twosided_str, twosided_val) && twosided_val >= 0) {
1442 twosided = (uint) twosided_val;
1443 }
1444 }
1445
1446 // Create the material using the new label-based API
1447 // Use internal method to bypass reserved label check for __auto_ labels
1448 if (!doesMaterialExist(material_label)) {
1449 uint newID = currentMaterialID++;
1450 Material loaded_mat(newID, material_label, color, texture_file, texture_override, twosided);
1451 materials[newID] = loaded_mat;
1452 material_label_to_id[material_label] = newID;
1453 } else {
1454 // Material already exists, update its properties
1455 setMaterialColor(material_label, color);
1456 if (!texture_file.empty()) {
1457 setMaterialTexture(material_label, texture_file);
1458 }
1459 setMaterialTextureColorOverride(material_label, texture_override);
1460 setMaterialTwosidedFlag(material_label, twosided);
1461 }
1462
1463 // Load material data
1464 loadMaterialData(mat, material_label);
1465 }
1466 }
1467
1468 //-------------- PATCHES ---------------//
1469 for (pugi::xml_node p = helios.child("patch"); p; p = p.next_sibling("patch")) {
1470 // * Patch Object ID * //
1471 uint objID = 0;
1472 if (XMLparser::parse_objID(p, objID) > 1) {
1473 helios_runtime_error("ERROR (Context::loadXML): Object ID (objID) given in 'patch' block must be a non-negative integer value.");
1474 }
1475
1476 // * Patch Transformation Matrix * //
1477 float transform[16];
1478 int result = XMLparser::parse_transform(p, transform);
1479 if (result == 3) {
1480 helios_runtime_error("ERROR (Context::loadXML): Patch <transform> node contains less than 16 data values.");
1481 } else if (result == 2) {
1482 helios_runtime_error("ERROR (Context::loadXML): Patch <transform> node contains invalid data.");
1483 }
1484
1485 // * Patch Texture * //
1486 std::string texture_file;
1487 XMLparser::parse_texture(p, texture_file);
1488
1489 // * Patch Texture (u,v) Coordinates * //
1490 std::vector<vec2> uv;
1491 if (XMLparser::parse_textureUV(p, uv) == 2) {
1492 helios_runtime_error("ERROR (Context::loadXML): (u,v) coordinates given in 'patch' block contain invalid data.");
1493 }
1494
1495 // * Patch Solid Fraction * //
1496 float solid_fraction = -1;
1497 if (XMLparser::parse_solid_fraction(p, solid_fraction) == 2) {
1498 helios_runtime_error("ERROR (Context::loadXML): Solid fraction given in 'patch' block contains invalid data.");
1499 }
1500
1501 // * Check for v3 material format (string label) vs v2 (numeric ID) vs legacy (color/texture) * //
1502 pugi::xml_node material_node = p.child("material");
1503 pugi::xml_node material_id_node = p.child("material_id");
1504 std::string material_label_from_xml;
1505 bool has_material = false;
1506
1507 if (!material_node.empty()) {
1508 // v3 format: <material>label</material>
1509 material_label_from_xml = deblank(material_node.child_value());
1510 if (!material_label_from_xml.empty() && doesMaterialExist(material_label_from_xml)) {
1511 has_material = true;
1512 ID = addPatch(make_vec3(0, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), make_RGBAcolor(0, 0, 0, 1));
1513 }
1514 } else if (!material_id_node.empty()) {
1515 // v2 format: <material_id>N</material_id>
1516 uint materialID_from_xml = 0;
1517 const char *mat_id_str = material_id_node.child_value();
1518 if (parse_uint(mat_id_str, materialID_from_xml)) {
1519 // Look up the label for this legacy numeric ID
1520 auto it = legacy_material_id_to_label.find(materialID_from_xml);
1521 if (it != legacy_material_id_to_label.end()) {
1522 material_label_from_xml = it->second;
1523 has_material = true;
1524 ID = addPatch(make_vec3(0, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), make_RGBAcolor(0, 0, 0, 1));
1525 }
1526 }
1527 }
1528
1529 if (!has_material) {
1530 // Legacy format: parse color and texture
1531 RGBAcolor color;
1532 pugi::xml_node color_node = p.child("color");
1533
1534 const char *color_str = color_node.child_value();
1535 if (strlen(color_str) == 0) {
1536 color = make_RGBAcolor(0, 0, 0, 1); // assume default color of black
1537 } else {
1538 color = string2RGBcolor(color_str);
1539 }
1540
1541 // * Add the Patch * //
1542 if (strcmp(texture_file.c_str(), "none") == 0) { // no texture file was given
1543 ID = addPatch(make_vec3(0, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), color);
1544 } else { // has a texture file
1545 std::string texture_file_copy;
1546 if (solid_fraction < 1.f && solid_fraction >= 0.f) { // solid fraction was given in the XML, and is not equal to 1.0
1547 texture_file_copy = texture_file;
1548 texture_file = "lib/images/solid.jpg"; // load dummy solid texture to avoid re-calculating the solid fraction
1549 }
1550 if (uv.empty()) { // custom (u,v) coordinates were not given
1551 ID = addPatch(make_vec3(0, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), texture_file.c_str());
1552 } else {
1553 ID = addPatch(make_vec3(0, 0, 0), make_vec2(1, 1), make_SphericalCoord(0, 0), texture_file.c_str(), 0.5 * (uv.at(2) + uv.at(0)), uv.at(2) - uv.at(0));
1554 }
1555 if (solid_fraction < 1.f && solid_fraction >= 0.f) { // replace dummy texture and set the solid fraction
1556 getPrimitivePointer_private(ID)->setTextureFile(texture_file_copy.c_str());
1557 addTexture(texture_file_copy.c_str());
1558 getPrimitivePointer_private(ID)->setSolidFraction(solid_fraction);
1559 }
1560 }
1561 }
1562
1563 getPrimitivePointer_private(ID)->setTransformationMatrix(transform);
1564
1565 // Assign material if using material format
1566 if (has_material && !material_label_from_xml.empty()) {
1567 assignMaterialToPrimitive(ID, material_label_from_xml);
1568 }
1569
1570 if (objID > 0) {
1571 object_prim_UUIDs[objID].push_back(ID);
1572 }
1573
1574 if (objID == 0) {
1575 UUID.push_back(ID);
1576 }
1577
1578 // * Primitive Data * //
1579
1580 loadPData(p, ID);
1581 } // end patches
1582
1583 //-------------- TRIANGLES ---------------//
1584
1585 // looping over any triangles specified in XML file
1586 for (pugi::xml_node tri = helios.child("triangle"); tri; tri = tri.next_sibling("triangle")) {
1587 // * Triangle Object ID * //
1588 uint objID = 0;
1589 if (XMLparser::parse_objID(tri, objID) > 1) {
1590 helios_runtime_error("ERROR (Context::loadXML): Object ID (objID) given in 'triangle' block must be a non-negative integer value.");
1591 }
1592
1593 // * Triangle Transformation Matrix * //
1594 float transform[16];
1595 int result = XMLparser::parse_transform(tri, transform);
1596 if (result == 3) {
1597 helios_runtime_error("ERROR (Context::loadXML): Triangle <transform> node contains less than 16 data values.");
1598 } else if (result == 2) {
1599 helios_runtime_error("ERROR (Context::loadXML): Triangle <transform> node contains invalid data.");
1600 }
1601
1602 // * Triangle Texture * //
1603 std::string texture_file;
1604 XMLparser::parse_texture(tri, texture_file);
1605
1606 // * Triangle Texture (u,v) Coordinates * //
1607 std::vector<vec2> uv;
1608 if (XMLparser::parse_textureUV(tri, uv) == 2) {
1609 helios_runtime_error("ERROR (Context::loadXML): (u,v) coordinates given in 'triangle' block contain invalid data.");
1610 }
1611
1612 // * Triangle Solid Fraction * //
1613 float solid_fraction = -1;
1614 if (XMLparser::parse_solid_fraction(tri, solid_fraction) == 2) {
1615 helios_runtime_error("ERROR (Context::loadXML): Solid fraction given in 'triangle' block contains invalid data.");
1616 }
1617
1618 // * Check for v3 material format (string label) vs v2 (numeric ID) vs legacy (color/texture) * //
1619 pugi::xml_node material_node_tri = tri.child("material");
1620 pugi::xml_node material_id_node_tri = tri.child("material_id");
1621 std::string material_label_from_xml_tri;
1622 bool has_material_tri = false;
1623
1624 if (!material_node_tri.empty()) {
1625 // v3 format: <material>label</material>
1626 material_label_from_xml_tri = deblank(material_node_tri.child_value());
1627 if (!material_label_from_xml_tri.empty() && doesMaterialExist(material_label_from_xml_tri)) {
1628 has_material_tri = true;
1629 }
1630 } else if (!material_id_node_tri.empty()) {
1631 // v2 format: <material_id>N</material_id>
1632 uint materialID_from_xml_tri = 0;
1633 const char *mat_id_str = material_id_node_tri.child_value();
1634 if (parse_uint(mat_id_str, materialID_from_xml_tri)) {
1635 // Look up the label for this legacy numeric ID
1636 auto it = legacy_material_id_to_label.find(materialID_from_xml_tri);
1637 if (it != legacy_material_id_to_label.end()) {
1638 material_label_from_xml_tri = it->second;
1639 has_material_tri = true;
1640 }
1641 }
1642 }
1643
1644 std::vector<vec3> vert_pos;
1645 vert_pos.resize(3);
1646 vert_pos.at(0) = make_vec3(0.f, 0.f, 0.f);
1647 vert_pos.at(1) = make_vec3(0.f, 1.f, 0.f);
1648 vert_pos.at(2) = make_vec3(1.f, 1.f, 0.f);
1649
1650 if (has_material_tri) {
1651 // Material format: create triangle with default color, will assign material below
1652 ID = addTriangle(vert_pos.at(0), vert_pos.at(1), vert_pos.at(2), make_RGBAcolor(0, 0, 0, 1));
1653 } else {
1654 // Legacy format: parse color and texture
1655 RGBAcolor color;
1656 pugi::xml_node color_node = tri.child("color");
1657
1658 const char *color_str = color_node.child_value();
1659 if (strlen(color_str) == 0) {
1660 color = make_RGBAcolor(0, 0, 0, 1); // assume default color of black
1661 } else {
1662 color = string2RGBcolor(color_str);
1663 }
1664
1665 // * Add the Triangle * //
1666 if (strcmp(texture_file.c_str(), "none") == 0 || uv.empty()) {
1667 ID = addTriangle(vert_pos.at(0), vert_pos.at(1), vert_pos.at(2), color);
1668 } else {
1669 std::string texture_file_copy;
1670 if (solid_fraction < 1.f && solid_fraction >= 0.f) { // solid fraction was given in the XML, and is not equal to 1.0
1671 texture_file_copy = texture_file;
1672 texture_file = "lib/images/solid.jpg"; // load dummy solid texture to avoid re-calculating the solid fraction
1673 }
1674 ID = addTriangle(vert_pos.at(0), vert_pos.at(1), vert_pos.at(2), texture_file.c_str(), uv.at(0), uv.at(1), uv.at(2));
1675 if (solid_fraction < 1.f && solid_fraction >= 0.f) {
1676 getPrimitivePointer_private(ID)->setTextureFile(texture_file_copy.c_str());
1677 addTexture(texture_file_copy.c_str());
1678 getPrimitivePointer_private(ID)->setSolidFraction(solid_fraction);
1679 }
1680 }
1681 }
1682
1683 getPrimitivePointer_private(ID)->setTransformationMatrix(transform);
1684
1685 // Assign material if using material format
1686 if (has_material_tri && !material_label_from_xml_tri.empty()) {
1687 assignMaterialToPrimitive(ID, material_label_from_xml_tri);
1688 }
1689
1690 if (objID > 0) {
1691 object_prim_UUIDs[objID].push_back(ID);
1692 }
1693
1694 if (objID == 0) {
1695 UUID.push_back(ID);
1696 }
1697
1698 // * Primitive Data * //
1699
1700 loadPData(tri, ID);
1701 }
1702
1703 //-------------- VOXELS ---------------//
1704 for (pugi::xml_node p = helios.child("voxel"); p; p = p.next_sibling("voxel")) {
1705 // * Voxel Object ID * //
1706 uint objID = 0;
1707 if (XMLparser::parse_objID(p, objID) > 1) {
1708 helios_runtime_error("ERROR (Context::loadXML): Object ID (objID) given in 'voxel' block must be a non-negative integer value.");
1709 }
1710
1711 // * Voxel Transformation Matrix * //
1712 float transform[16];
1713 int result = XMLparser::parse_transform(p, transform);
1714 if (result == 3) {
1715 helios_runtime_error("ERROR (Context::loadXML): Voxel <transform> node contains less than 16 data values.");
1716 } else if (result == 2) {
1717 helios_runtime_error("ERROR (Context::loadXML): Voxel <transform> node contains invalid data.");
1718 }
1719
1720 // * Voxel Solid Fraction * //
1721 float solid_fraction = 1;
1722 if (XMLparser::parse_solid_fraction(p, solid_fraction) == 2) {
1723 helios_runtime_error("ERROR (Context::loadXML): Solid fraction given in 'voxel' block contains invalid data.");
1724 }
1725
1726 // * Check for v3 material format (string label) vs v2 (numeric ID) vs legacy (color/texture) * //
1727 pugi::xml_node material_node_vox = p.child("material");
1728 pugi::xml_node material_id_node_vox = p.child("material_id");
1729 std::string material_label_from_xml_vox;
1730 bool has_material_vox = false;
1731
1732 if (!material_node_vox.empty()) {
1733 // v3 format: <material>label</material>
1734 material_label_from_xml_vox = deblank(material_node_vox.child_value());
1735 if (!material_label_from_xml_vox.empty() && doesMaterialExist(material_label_from_xml_vox)) {
1736 has_material_vox = true;
1737 }
1738 } else if (!material_id_node_vox.empty()) {
1739 // v2 format: <material_id>N</material_id>
1740 uint materialID_from_xml_vox = 0;
1741 const char *mat_id_str = material_id_node_vox.child_value();
1742 if (parse_uint(mat_id_str, materialID_from_xml_vox)) {
1743 // Look up the label for this legacy numeric ID
1744 auto it = legacy_material_id_to_label.find(materialID_from_xml_vox);
1745 if (it != legacy_material_id_to_label.end()) {
1746 material_label_from_xml_vox = it->second;
1747 has_material_vox = true;
1748 }
1749 }
1750 }
1751
1752 if (has_material_vox) {
1753 // Material format: create voxel with default color, will assign material below
1754 ID = addVoxel(make_vec3(0, 0, 0), make_vec3(0, 0, 0), 0, make_RGBAcolor(0, 0, 0, 1));
1755 } else {
1756 // Legacy format: parse color
1757 RGBAcolor color;
1758 pugi::xml_node color_node = p.child("color");
1759
1760 const char *color_str = color_node.child_value();
1761 if (strlen(color_str) == 0) {
1762 color = make_RGBAcolor(0, 0, 0, 1); // assume default color of black
1763 } else {
1764 color = string2RGBcolor(color_str);
1765 }
1766
1767 // * Add the Voxel * //
1768 ID = addVoxel(make_vec3(0, 0, 0), make_vec3(0, 0, 0), 0, color);
1769 }
1770
1771 getPrimitivePointer_private(ID)->setTransformationMatrix(transform);
1772
1773 // Assign material if using material format
1774 if (has_material_vox && !material_label_from_xml_vox.empty()) {
1775 assignMaterialToPrimitive(ID, material_label_from_xml_vox);
1776 }
1777
1778 if (objID > 0) {
1779 object_prim_UUIDs[objID].push_back(ID);
1780 }
1781
1782 if (objID == 0) {
1783 UUID.push_back(ID);
1784 }
1785
1786 // * Primitive Data * //
1787
1788 loadPData(p, ID);
1789 }
1790
1791 //-------------- COMPOUND OBJECTS ---------------//
1792
1793 //-------------- TILES ---------------//
1794 for (pugi::xml_node p = helios.child("tile"); p; p = p.next_sibling("tile")) {
1795 // * Tile Object ID * //
1796 uint objID = 0;
1797 if (XMLparser::parse_objID(p, objID) > 1) {
1798 helios_runtime_error("ERROR (Context::loadXML): Object ID (objID) given in 'tile' block must be a non-negative integer value.");
1799 }
1800
1801 // * Tile Transformation Matrix * //
1802 float transform[16];
1803 int result = XMLparser::parse_transform(p, transform);
1804 if (result == 3) {
1805 helios_runtime_error("ERROR (Context::loadXML): Tile <transform> node contains less than 16 data values.");
1806 } else if (result == 2) {
1807 helios_runtime_error("ERROR (Context::loadXML): Tile <transform> node contains invalid data.");
1808 }
1809
1810 // * Tile Texture * //
1811 std::string texture_file;
1812 XMLparser::parse_texture(p, texture_file);
1813
1814 // * Tile Texture (u,v) Coordinates * //
1815 std::vector<vec2> uv;
1816 if (XMLparser::parse_textureUV(p, uv) == 2) {
1817 helios_runtime_error("ERROR (Context::loadXML): (u,v) coordinates given in 'tile' block contain invalid data.");
1818 }
1819
1820 // * Tile Diffuse Colors * //
1821 RGBAcolor color;
1822 pugi::xml_node color_node = p.child("color");
1823
1824 const char *color_str = color_node.child_value();
1825 if (strlen(color_str) != 0) {
1826 color = string2RGBcolor(color_str);
1827 }
1828
1829 // * Tile Subdivisions * //
1830 int2 subdiv;
1831 int result_subdiv = XMLparser::parse_subdivisions(p, subdiv);
1832 if (result_subdiv == 1) {
1833 std::cerr << "WARNING (Context::loadXML): Number of subdivisions for tile was not provided. Assuming 1x1." << std::endl;
1834 subdiv = make_int2(1, 1);
1835 } else if (result_subdiv == 2) {
1836 helios_runtime_error("ERROR (Context::loadXML): Tile <subdivisions> node contains invalid data. ");
1837 }
1838
1839 // Create a dummy patch in order to get the center, size, and rotation based on transformation matrix
1840 Patch patch(make_RGBAcolor(0, 0, 0, 0), 0, 0);
1841 patch.setTransformationMatrix(transform);
1842 // SphericalCoord rotation = cart2sphere(patch.getNormal());
1843 // rotation.elevation = rotation.zenith;
1844
1845 // * Add the Tile * //
1846 // if (strcmp(texture_file.c_str(), "none") == 0) {
1847 // if( strlen(color_str) == 0 ){
1848 // ID = addTileObject(patch.getCenter(), patch.getSize(), rotation, subdiv );
1849 // }else {
1850 // ID = addTileObject(patch.getCenter(), patch.getSize(), rotation, subdiv, make_RGBcolor(color.r, color.g, color.b));
1851 // }
1852 // } else {
1853 // ID = addTileObject(patch.getCenter(), patch.getSize(), rotation, subdiv, texture_file.c_str());
1854 // }
1855
1856 if (strcmp(texture_file.c_str(), "none") == 0) {
1857 if (strlen(color_str) == 0) {
1858 ID = addTileObject(make_vec3(0, 0, 0), make_vec2(1, 1), nullrotation, subdiv);
1859 } else {
1860 ID = addTileObject(make_vec3(0, 0, 0), make_vec2(1, 1), nullrotation, subdiv, make_RGBcolor(color.r, color.g, color.b));
1861 }
1862 } else {
1863 ID = addTileObject(make_vec3(0, 0, 0), make_vec2(1, 1), nullrotation, subdiv, texture_file.c_str());
1864 }
1865
1866 getTileObjectPointer_private(ID)->setTransformationMatrix(transform);
1867
1869
1870 // if primitives exist that were assigned to this object, delete all primitives that were just created
1871 if (objID > 0 && !object_prim_UUIDs.empty() && object_prim_UUIDs.find(objID) != object_prim_UUIDs.end()) {
1872 std::vector<uint> uuids_to_delete = getObjectPrimitiveUUIDs(ID);
1873 getObjectPointer_private(ID)->setPrimitiveUUIDs(object_prim_UUIDs.at(objID));
1874 deletePrimitive(uuids_to_delete);
1875 // \todo This is fairly inefficient, it would be nice to have a way to do this without having to create and delete a bunch of primitives
1876 }
1877
1879
1880 // * Tile Sub-Patch Data * //
1881
1882 loadOsubPData(p, ID);
1883
1884 // * Tile Object Data * //
1885
1886 loadOData(p, ID);
1887
1888 std::vector<uint> childUUIDs = getObjectPrimitiveUUIDs(ID);
1889 UUID.insert(UUID.end(), childUUIDs.begin(), childUUIDs.end());
1890 } // end tiles
1891
1892 //-------------- SPHERES ---------------//
1893 for (pugi::xml_node p = helios.child("sphere"); p; p = p.next_sibling("sphere")) {
1894 // * Sphere Object ID * //
1895 uint objID = 0;
1896 if (XMLparser::parse_objID(p, objID) > 1) {
1897 helios_runtime_error("ERROR (Context::loadXML): Object ID (objID) given in 'sphere' block must be a non-negative integer value.");
1898 }
1899
1900 if (doesObjectExist(objID)) { // if this object ID is already in use, assign a new one
1901 objID = currentObjectID;
1902 currentObjectID++;
1903 }
1904
1905 // * Sphere Transformation Matrix * //
1906 float transform[16];
1907 int result = XMLparser::parse_transform(p, transform);
1908 if (result == 3) {
1909 helios_runtime_error("ERROR (Context::loadXML): Sphere <transform> node contains less than 16 data values.");
1910 } else if (result == 2) {
1911 helios_runtime_error("ERROR (Context::loadXML): Sphere <transform> node contains invalid data.");
1912 }
1913
1914 // * Sphere Texture * //
1915 std::string texture_file;
1916 XMLparser::parse_texture(p, texture_file);
1917
1918 // * Sphere Diffuse Colors * //
1919 RGBAcolor color;
1920 pugi::xml_node color_node = p.child("color");
1921
1922 const char *color_str = color_node.child_value();
1923 if (strlen(color_str) != 0) {
1924 color = string2RGBcolor(color_str);
1925 }
1926
1927 // * Sphere Subdivisions * //
1928 uint subdiv;
1929 int result_subdiv = XMLparser::parse_subdivisions(p, subdiv);
1930 if (result_subdiv == 1) {
1931 std::cerr << "WARNING (Context::loadXML): Number of subdivisions for sphere was not provided. Assuming 1x1." << std::endl;
1932 subdiv = 5;
1933 } else if (result_subdiv == 2) {
1934 helios_runtime_error("ERROR (Context::loadXML): Sphere <subdivisions> node contains invalid data. ");
1935 }
1936
1937 // Create a dummy sphere in order to get the center and radius based on transformation matrix
1938 std::vector<uint> empty;
1939 Sphere sphere(0, empty, 3, "", this);
1940 sphere.setTransformationMatrix(transform);
1941
1942 // * Add the Sphere * //
1943 if (strcmp(texture_file.c_str(), "none") == 0) {
1944 if (strlen(color_str) == 0) {
1945 ID = addSphereObject(subdiv, sphere.getCenter(), sphere.getRadius());
1946 } else {
1947 ID = addSphereObject(subdiv, sphere.getCenter(), sphere.getRadius(), make_RGBcolor(color.r, color.g, color.b));
1948 }
1949 } else {
1950 ID = addSphereObject(subdiv, sphere.getCenter(), sphere.getRadius(), texture_file.c_str());
1951 }
1952
1953 // if primitives exist that were assigned to this object, delete all primitives that were just created
1954 if (objID > 0 && object_prim_UUIDs.find(objID) != object_prim_UUIDs.end()) {
1955 std::vector<uint> uuids_to_delete = getObjectPrimitiveUUIDs(ID);
1956 getObjectPointer_private(ID)->setPrimitiveUUIDs(object_prim_UUIDs.at(objID));
1957 deletePrimitive(uuids_to_delete);
1958 // if( !doesObjectExist(ID) ){ //if the above method deleted all primitives for this object, move on
1959 // continue;
1960 // }
1961 }
1962
1964
1965 // * Sphere Sub-Triangle Data * //
1966
1967 loadOsubPData(p, ID);
1968
1969 // * Sphere Object Data * //
1970
1971 loadOData(p, ID);
1972
1973 std::vector<uint> childUUIDs = getObjectPrimitiveUUIDs(ID);
1974 UUID.insert(UUID.end(), childUUIDs.begin(), childUUIDs.end());
1975 } // end spheres
1976
1977 //-------------- TUBES ---------------//
1978 for (pugi::xml_node p = helios.child("tube"); p; p = p.next_sibling("tube")) {
1979 // * Tube Object ID * //
1980 uint objID = 0;
1981 if (XMLparser::parse_objID(p, objID) > 1) {
1982 helios_runtime_error("ERROR (Context::loadXML): Object ID (objID) given in 'tube' block must be a non-negative integer value.");
1983 }
1984
1985 if (doesObjectExist(objID)) { // if this object ID is already in use, assign a new one
1986 objID = currentObjectID;
1987 currentObjectID++;
1988 }
1989
1990 // * Tube Transformation Matrix * //
1991 float transform[16];
1992 int result = XMLparser::parse_transform(p, transform);
1993 if (result == 3) {
1994 helios_runtime_error("ERROR (Context::loadXML): Tube <transform> node contains less than 16 data values.");
1995 } else if (result == 2) {
1996 helios_runtime_error("ERROR (Context::loadXML): Tube <transform> node contains invalid data.");
1997 }
1998
1999 // * Tube Texture * //
2000 std::string texture_file;
2001 XMLparser::parse_texture(p, texture_file);
2002
2003 // * Tube Subdivisions * //
2004 uint subdiv;
2005 int result_subdiv = XMLparser::parse_subdivisions(p, subdiv);
2006 if (result_subdiv == 1) {
2007 std::cerr << "WARNING (Context::loadXML): Number of subdivisions for tube was not provided. Assuming 1x1." << std::endl;
2008 subdiv = 5;
2009 } else if (result_subdiv == 2) {
2010 helios_runtime_error("ERROR (Context::loadXML): Tube <subdivisions> node contains invalid data. ");
2011 }
2012
2013 // * Tube Nodes * //
2014 std::vector<vec3> nodes;
2015 pugi::xml_node nodes_node = p.child("nodes");
2016 if (XMLparser::parse_data_vec3(nodes_node, nodes) != 0 || nodes.size() < 2) {
2017 helios_runtime_error("ERROR (Context::loadXML): Tube <nodes> node contains invalid data. ");
2018 }
2019
2020 // * Tube Radius * //
2021 std::vector<float> radii;
2022 pugi::xml_node radii_node = p.child("radius");
2023 if (XMLparser::parse_data_float(radii_node, radii) != 0 || radii.size() < 2) {
2024 helios_runtime_error("ERROR (Context::loadXML): Tube <radius> node contains invalid data. ");
2025 }
2026
2027 // * Tube Color * //
2028
2029 pugi::xml_node color_node = p.child("color");
2030 const char *color_str = color_node.child_value();
2031
2032 std::vector<RGBcolor> colors;
2033 if (strlen(color_str) > 0) {
2034 std::istringstream data_stream(color_str);
2035 std::vector<float> tmp;
2036 tmp.resize(3);
2037 int c = 0;
2038 while (data_stream >> tmp.at(c)) {
2039 c++;
2040 if (c == 3) {
2041 colors.push_back(make_RGBcolor(tmp.at(0), tmp.at(1), tmp.at(2)));
2042 c = 0;
2043 }
2044 }
2045 }
2046
2047 // * Add the Tube * //
2048 if (texture_file == "none") {
2049 ID = addTubeObject(subdiv, nodes, radii, colors);
2050 } else {
2051 ID = addTubeObject(subdiv, nodes, radii, texture_file.c_str());
2052 }
2053
2054 getObjectPointer_private(ID)->setTransformationMatrix(transform);
2055
2056 // if primitives exist that were assigned to this object, delete all primitives that were just created
2057 if (objID > 0 && object_prim_UUIDs.find(objID) != object_prim_UUIDs.end()) {
2058 std::vector<uint> uuids_to_delete = getObjectPrimitiveUUIDs(ID);
2059 getObjectPointer_private(ID)->setPrimitiveUUIDs(object_prim_UUIDs.at(objID));
2060 deletePrimitive(uuids_to_delete);
2061 // if( !doesObjectExist(ID) ){ //if the above method deleted all primitives for this object, move on
2062 // continue;
2063 // }
2064 }
2065
2067
2068 // * Tube Sub-Triangle Data * //
2069
2070 loadOsubPData(p, ID);
2071
2072 // * tube Object Data * //
2073
2074 loadOData(p, ID);
2075
2076 std::vector<uint> childUUIDs = getObjectPrimitiveUUIDs(ID);
2077 UUID.insert(UUID.end(), childUUIDs.begin(), childUUIDs.end());
2078 } // end tubes
2079
2080 //-------------- BOXES ---------------//
2081 for (pugi::xml_node p = helios.child("box"); p; p = p.next_sibling("box")) {
2082 // * Box Object ID * //
2083 uint objID = 0;
2084 if (XMLparser::parse_objID(p, objID) > 1) {
2085 helios_runtime_error("ERROR (Context::loadXML): Object ID (objID) given in 'box' block must be a non-negative integer value.");
2086 }
2087
2088 if (doesObjectExist(objID)) { // if this object ID is already in use, assign a new one
2089 objID = currentObjectID;
2090 currentObjectID++;
2091 }
2092
2093 // * Box Transformation Matrix * //
2094 float transform[16];
2095 int result = XMLparser::parse_transform(p, transform);
2096 if (result == 3) {
2097 helios_runtime_error("ERROR (Context::loadXML): Box <transform> node contains less than 16 data values.");
2098 } else if (result == 2) {
2099 helios_runtime_error("ERROR (Context::loadXML): Box <transform> node contains invalid data.");
2100 }
2101
2102 // * Box Texture * //
2103 std::string texture_file;
2104 XMLparser::parse_texture(p, texture_file);
2105
2106 // * Box Diffuse Colors * //
2107 RGBAcolor color;
2108 pugi::xml_node color_node = p.child("color");
2109
2110 const char *color_str = color_node.child_value();
2111 if (strlen(color_str) != 0) {
2112 color = string2RGBcolor(color_str);
2113 }
2114
2115 // * Box Subdivisions * //
2116 int3 subdiv;
2117 int result_subdiv = XMLparser::parse_subdivisions(p, subdiv);
2118 if (result_subdiv == 1) {
2119 std::cerr << "WARNING (Context::loadXML): Number of subdivisions for box was not provided. Assuming 1x1." << std::endl;
2120 subdiv = make_int3(1, 1, 1);
2121 } else if (result_subdiv == 2) {
2122 helios_runtime_error("ERROR (Context::loadXML): Box <subdivisions> node contains invalid data. ");
2123 }
2124
2125 // Create a dummy box in order to get the center and size based on transformation matrix
2126 std::vector<uint> empty;
2127 Box box(0, empty, make_int3(1, 1, 1), "", this);
2128 box.setTransformationMatrix(transform);
2129
2130 // * Add the box * //
2131 if (strcmp(texture_file.c_str(), "none") == 0) {
2132 if (strlen(color_str) == 0) {
2133 ID = addBoxObject(box.getCenter(), box.getSize(), subdiv);
2134 } else {
2135 ID = addBoxObject(box.getCenter(), box.getSize(), subdiv, make_RGBcolor(color.r, color.g, color.b));
2136 }
2137 } else {
2138 ID = addBoxObject(box.getCenter(), box.getSize(), subdiv, texture_file.c_str());
2139 }
2140
2141 // if primitives exist that were assigned to this object, delete all primitives that were just created
2142 if (objID > 0 && object_prim_UUIDs.find(objID) != object_prim_UUIDs.end()) {
2143 std::vector<uint> uuids_to_delete = getObjectPrimitiveUUIDs(ID);
2144 getObjectPointer_private(ID)->setPrimitiveUUIDs(object_prim_UUIDs.at(objID));
2145 deletePrimitive(uuids_to_delete);
2146 // if( !doesObjectExist(ID) ){ //if the above method deleted all primitives for this object, move on
2147 // continue;
2148 // }
2149 }
2150
2152
2153 // * Box Sub-Patch Data * //
2154
2155 loadOsubPData(p, ID);
2156
2157 // * Box Object Data * //
2158
2159 loadOData(p, ID);
2160
2161 std::vector<uint> childUUIDs = getObjectPrimitiveUUIDs(ID);
2162 UUID.insert(UUID.end(), childUUIDs.begin(), childUUIDs.end());
2163 } // end boxes
2164
2165 //-------------- DISKS ---------------//
2166 for (pugi::xml_node p = helios.child("disk"); p; p = p.next_sibling("disk")) {
2167 // * Disk Object ID * //
2168 uint objID = 0;
2169 if (XMLparser::parse_objID(p, objID) > 1) {
2170 helios_runtime_error("ERROR (Context::loadXML): Object ID (objID) given in 'disk' block must be a non-negative integer value.");
2171 }
2172
2173 if (doesObjectExist(objID)) { // if this object ID is already in use, assign a new one
2174 objID = currentObjectID;
2175 currentObjectID++;
2176 }
2177
2178 // * Disk Transformation Matrix * //
2179 float transform[16];
2180 int result = XMLparser::parse_transform(p, transform);
2181 if (result == 3) {
2182 helios_runtime_error("ERROR (Context::loadXML): Disk <transform> node contains less than 16 data values.");
2183 } else if (result == 2) {
2184 helios_runtime_error("ERROR (Context::loadXML): Disk <transform> node contains invalid data.");
2185 }
2186
2187 // * Disk Texture * //
2188 std::string texture_file;
2189 XMLparser::parse_texture(p, texture_file);
2190
2191 // * Disk Diffuse Colors * //
2192 RGBAcolor color;
2193 pugi::xml_node color_node = p.child("color");
2194
2195 const char *color_str = color_node.child_value();
2196 if (strlen(color_str) != 0) {
2197 color = string2RGBcolor(color_str);
2198 }
2199
2200 // * Disk Subdivisions * //
2201 int2 subdiv;
2202 int result_subdiv = XMLparser::parse_subdivisions(p, subdiv);
2203 if (result_subdiv == 1) {
2204 std::cerr << "WARNING (Context::loadXML): Number of subdivisions for disk was not provided. Assuming 1x1." << std::endl;
2205 subdiv = make_int2(5, 1);
2206 } else if (result_subdiv == 2) {
2207 helios_runtime_error("ERROR (Context::loadXML): Disk <subdivisions> node contains invalid data. ");
2208 }
2209
2210 // Create a dummy disk in order to get the center and size based on transformation matrix
2211 std::vector<uint> empty;
2212 Disk disk(0, empty, make_int2(1, 1), "", this);
2213 disk.setTransformationMatrix(transform);
2214
2215 // * Add the disk * //
2216 if (strcmp(texture_file.c_str(), "none") == 0) {
2217 if (strlen(color_str) == 0) {
2218 ID = addDiskObject(subdiv, disk.getCenter(), disk.getSize(), nullrotation, RGB::red);
2219 } else {
2220 ID = addDiskObject(subdiv, disk.getCenter(), disk.getSize(), nullrotation, make_RGBcolor(color.r, color.g, color.b));
2221 }
2222 } else {
2223 ID = addDiskObject(subdiv, disk.getCenter(), disk.getSize(), nullrotation, texture_file.c_str());
2224 }
2225
2226 // if primitives exist that were assigned to this object, delete all primitives that were just created
2227 if (objID > 0 && object_prim_UUIDs.find(objID) != object_prim_UUIDs.end()) {
2228 std::vector<uint> uuids_to_delete = getObjectPrimitiveUUIDs(ID);
2229 getObjectPointer_private(ID)->setPrimitiveUUIDs(object_prim_UUIDs.at(objID));
2230 deletePrimitive(uuids_to_delete);
2231 // if( !doesObjectExist(ID) ){ //if the above method deleted all primitives for this object, move on
2232 // continue;
2233 // }
2234 }
2235
2237
2238 // * Disk Sub-Triangle Data * //
2239
2240 loadOsubPData(p, ID);
2241
2242 // * Disk Object Data * //
2243
2244 loadOData(p, ID);
2245
2246 std::vector<uint> childUUIDs = getObjectPrimitiveUUIDs(ID);
2247 UUID.insert(UUID.end(), childUUIDs.begin(), childUUIDs.end());
2248 } // end disks
2249
2250 //-------------- CONES ---------------//
2251 for (pugi::xml_node p = helios.child("cone"); p; p = p.next_sibling("cone")) {
2252 // * Cone Object ID * //
2253 uint objID = 0;
2254 if (XMLparser::parse_objID(p, objID) > 1) {
2255 helios_runtime_error("ERROR (Context::loadXML): Object ID (objID) given in 'cone' block must be a non-negative integer value.");
2256 }
2257
2258 if (doesObjectExist(objID)) { // if this object ID is already in use, assign a new one
2259 objID = currentObjectID;
2260 currentObjectID++;
2261 }
2262
2263 // * Cone Transformation Matrix * //
2264 float transform[16];
2265 int result = XMLparser::parse_transform(p, transform);
2266 if (result == 3) {
2267 helios_runtime_error("ERROR (Context::loadXML): Cone <transform> node contains less than 16 data values.");
2268 } else if (result == 2) {
2269 helios_runtime_error("ERROR (Context::loadXML): Cone <transform> node contains invalid data.");
2270 }
2271
2272 // * Cone Texture * //
2273 std::string texture_file;
2274 XMLparser::parse_texture(p, texture_file);
2275
2276 // * Cone Diffuse Colors * //
2277 RGBAcolor color;
2278 pugi::xml_node color_node = p.child("color");
2279
2280 const char *color_str = color_node.child_value();
2281 if (strlen(color_str) != 0) {
2282 color = string2RGBcolor(color_str);
2283 }
2284
2285 // * Cone Subdivisions * //
2286 uint subdiv;
2287 int result_subdiv = XMLparser::parse_subdivisions(p, subdiv);
2288 if (result_subdiv == 1) {
2289 std::cerr << "WARNING (Context::loadXML): Number of subdivisions for cone was not provided. Assuming 1x1." << std::endl;
2290 subdiv = 5;
2291 } else if (result_subdiv == 2) {
2292 helios_runtime_error("ERROR (Context::loadXML): Cone <subdivisions> node contains invalid data. ");
2293 }
2294
2295 // * Cone Nodes * //
2296 std::vector<vec3> nodes;
2297 pugi::xml_node nodes_node = p.child("nodes");
2298 if (XMLparser::parse_data_vec3(nodes_node, nodes) != 0 || nodes.size() != 2) {
2299 helios_runtime_error("ERROR (Context::loadXML): Cone <nodes> node contains invalid data. ");
2300 }
2301
2302 // * Cone Radius * //
2303 std::vector<float> radii;
2304 pugi::xml_node radii_node = p.child("radius");
2305 if (XMLparser::parse_data_float(radii_node, radii) != 0 || radii.size() != 2) {
2306 helios_runtime_error("ERROR (Context::loadXML): Cone <radius> node contains invalid data. ");
2307 }
2308
2309 // * Add the Cone * //
2310 if (texture_file == "none") {
2311 ID = addConeObject(subdiv, nodes.at(0), nodes.at(1), radii.at(0), radii.at(1), make_RGBcolor(color.r, color.g, color.b));
2312 } else {
2313 ID = addConeObject(subdiv, nodes.at(0), nodes.at(1), radii.at(0), radii.at(1), texture_file.c_str());
2314 }
2315
2316 getObjectPointer_private(ID)->setTransformationMatrix(transform);
2317
2318 // if primitives exist that were assigned to this object, delete all primitives that were just created
2319 if (objID > 0 && object_prim_UUIDs.find(objID) != object_prim_UUIDs.end()) {
2320 std::vector<uint> uuids_to_delete = getObjectPrimitiveUUIDs(ID);
2321 getObjectPointer_private(ID)->setPrimitiveUUIDs(object_prim_UUIDs.at(objID));
2322 deletePrimitive(uuids_to_delete);
2323 // if( !doesObjectExist(ID) ){ //if the above method deleted all primitives for this object, move on
2324 // continue;
2325 // }
2326 }
2327
2329
2330 // * Cone Sub-Triangle Data * //
2331
2332 loadOsubPData(p, ID);
2333
2334 // * Cone Object Data * //
2335
2336 loadOData(p, ID);
2337
2338 std::vector<uint> childUUIDs = getObjectPrimitiveUUIDs(ID);
2339 UUID.insert(UUID.end(), childUUIDs.begin(), childUUIDs.end());
2340 } // end cones
2341
2342 //-------------- POLYMESH ---------------//
2343 for (pugi::xml_node p = helios.child("polymesh"); p; p = p.next_sibling("polymesh")) {
2344 // * Polymesh Object ID * //
2345 uint objID = 0;
2346 if (XMLparser::parse_objID(p, objID) > 1) {
2347 helios_runtime_error("ERROR (Context::loadXML): Object ID (objID) given in 'polymesh' block must be a non-negative integer value.");
2348 }
2349
2350 if (doesObjectExist(objID)) { // if this object ID is already in use, assign a new one
2351 objID = currentObjectID;
2352 currentObjectID++;
2353 }
2354
2355 ID = addPolymeshObject(object_prim_UUIDs.at(objID));
2356
2357 setPrimitiveParentObjectID(object_prim_UUIDs.at(objID), ID);
2358
2359 // * Polymesh Sub-Primitive Data * //
2360
2361 loadOsubPData(p, ID);
2362
2363 // * Polymesh Object Data * //
2364
2365 loadOData(p, ID);
2366
2367 std::vector<uint> childUUIDs = object_prim_UUIDs.at(objID);
2368 UUID.insert(UUID.end(), childUUIDs.begin(), childUUIDs.end());
2369 } // end polymesh
2370
2371 //-------------- GLOBAL DATA ---------------//
2372
2373 for (pugi::xml_node data = helios.child("globaldata_int"); data; data = data.next_sibling("globaldata_int")) {
2374 const char *label = data.attribute("label").value();
2375
2376 std::vector<int> datav;
2377 if (XMLparser::parse_data_int(data, datav) != 0) {
2378 helios_runtime_error("ERROR (Context::loadXML): Global data tag <globaldata_int> with label " + std::string(label) + " contained invalid data.");
2379 }
2380
2381 if (datav.size() == 1) {
2382 setGlobalData(label, datav.front());
2383 } else if (datav.size() > 1) {
2384 setGlobalData(label, datav);
2385 }
2386 }
2387
2388 for (pugi::xml_node data = helios.child("globaldata_uint"); data; data = data.next_sibling("globaldata_uint")) {
2389 const char *label = data.attribute("label").value();
2390
2391 std::vector<uint> datav;
2392 if (XMLparser::parse_data_uint(data, datav) != 0) {
2393 helios_runtime_error("ERROR (Context::loadXML): Global data tag <globaldata_uint> with label " + std::string(label) + " contained invalid data.");
2394 }
2395
2396 if (datav.size() == 1) {
2397 setGlobalData(label, datav.front());
2398 } else if (datav.size() > 1) {
2399 setGlobalData(label, datav);
2400 }
2401 }
2402
2403 for (pugi::xml_node data = helios.child("globaldata_float"); data; data = data.next_sibling("globaldata_float")) {
2404 const char *label = data.attribute("label").value();
2405
2406 std::vector<float> datav;
2407 if (XMLparser::parse_data_float(data, datav) != 0) {
2408 helios_runtime_error("ERROR (Context::loadXML): Global data tag <globaldata_float> with label " + std::string(label) + " contained invalid data.");
2409 }
2410
2411 if (datav.size() == 1) {
2412 setGlobalData(label, datav.front());
2413 } else if (datav.size() > 1) {
2414 setGlobalData(label, datav);
2415 }
2416 }
2417
2418 for (pugi::xml_node data = helios.child("globaldata_double"); data; data = data.next_sibling("globaldata_double")) {
2419 const char *label = data.attribute("label").value();
2420
2421 std::vector<double> datav;
2422 if (XMLparser::parse_data_double(data, datav) != 0) {
2423 helios_runtime_error("ERROR (Context::loadXML): Global data tag <globaldata_double> with label " + std::string(label) + " contained invalid data.");
2424 }
2425
2426 if (datav.size() == 1) {
2427 setGlobalData(label, datav.front());
2428 } else if (datav.size() > 1) {
2429 setGlobalData(label, datav);
2430 }
2431 }
2432
2433 for (pugi::xml_node data = helios.child("globaldata_vec2"); data; data = data.next_sibling("globaldata_vec2")) {
2434 const char *label = data.attribute("label").value();
2435
2436 std::vector<vec2> datav;
2437 if (XMLparser::parse_data_vec2(data, datav) != 0) {
2438 helios_runtime_error("ERROR (Context::loadXML): Global data tag <globaldata_vec2> with label " + std::string(label) + " contained invalid data.");
2439 }
2440
2441 if (datav.size() == 1) {
2442 setGlobalData(label, datav.front());
2443 } else if (datav.size() > 1) {
2444 setGlobalData(label, datav);
2445 }
2446 }
2447
2448 for (pugi::xml_node data = helios.child("globaldata_vec3"); data; data = data.next_sibling("globaldata_vec3")) {
2449 const char *label = data.attribute("label").value();
2450
2451 std::vector<vec3> datav;
2452 if (XMLparser::parse_data_vec3(data, datav) != 0) {
2453 helios_runtime_error("ERROR (Context::loadXML): Global data tag <globaldata_vec3> with label " + std::string(label) + " contained invalid data.");
2454 }
2455
2456 if (datav.size() == 1) {
2457 setGlobalData(label, datav.front());
2458 } else if (datav.size() > 1) {
2459 setGlobalData(label, datav);
2460 }
2461 }
2462
2463 for (pugi::xml_node data = helios.child("globaldata_vec4"); data; data = data.next_sibling("globaldata_vec4")) {
2464 const char *label = data.attribute("label").value();
2465
2466 std::vector<vec4> datav;
2467 if (XMLparser::parse_data_vec4(data, datav) != 0) {
2468 helios_runtime_error("ERROR (Context::loadXML): Global data tag <globaldata_vec4> with label " + std::string(label) + " contained invalid data.");
2469 }
2470
2471 if (datav.size() == 1) {
2472 setGlobalData(label, datav.front());
2473 } else if (datav.size() > 1) {
2474 setGlobalData(label, datav);
2475 }
2476 }
2477
2478 for (pugi::xml_node data = helios.child("globaldata_int2"); data; data = data.next_sibling("globaldata_int2")) {
2479 const char *label = data.attribute("label").value();
2480
2481 std::vector<int2> datav;
2482 if (XMLparser::parse_data_int2(data, datav) != 0) {
2483 helios_runtime_error("ERROR (Context::loadXML): Global data tag <globaldata_int2> with label " + std::string(label) + " contained invalid data.");
2484 }
2485
2486 if (datav.size() == 1) {
2487 setGlobalData(label, datav.front());
2488 } else if (datav.size() > 1) {
2489 setGlobalData(label, datav);
2490 }
2491 }
2492
2493 for (pugi::xml_node data = helios.child("globaldata_int3"); data; data = data.next_sibling("globaldata_int3")) {
2494 const char *label = data.attribute("label").value();
2495
2496 std::vector<int3> datav;
2497 if (XMLparser::parse_data_int3(data, datav) != 0) {
2498 helios_runtime_error("ERROR (Context::loadXML): Global data tag <globaldata_int3> with label " + std::string(label) + " contained invalid data.");
2499 }
2500
2501 if (datav.size() == 1) {
2502 setGlobalData(label, datav.front());
2503 } else if (datav.size() > 1) {
2504 setGlobalData(label, datav);
2505 }
2506 }
2507
2508 for (pugi::xml_node data = helios.child("globaldata_int4"); data; data = data.next_sibling("globaldata_int4")) {
2509 const char *label = data.attribute("label").value();
2510
2511 std::vector<int4> datav;
2512 if (XMLparser::parse_data_int4(data, datav) != 0) {
2513 helios_runtime_error("ERROR (Context::loadXML): Global data tag <globaldata_int4> with label " + std::string(label) + " contained invalid data.");
2514 }
2515
2516 if (datav.size() == 1) {
2517 setGlobalData(label, datav.front());
2518 } else if (datav.size() > 1) {
2519 setGlobalData(label, datav);
2520 }
2521 }
2522
2523 for (pugi::xml_node data = helios.child("globaldata_string"); data; data = data.next_sibling("globaldata_string")) {
2524 const char *label = data.attribute("label").value();
2525
2526 std::vector<std::string> datav;
2527 if (XMLparser::parse_data_string(data, datav) != 0) {
2528 helios_runtime_error("ERROR (Context::loadXML): Global data tag <globaldata_string> with label " + std::string(label) + " contained invalid data.");
2529 }
2530
2531 if (datav.size() == 1) {
2532 setGlobalData(label, datav.front());
2533 } else if (datav.size() > 1) {
2534 setGlobalData(label, datav);
2535 }
2536 }
2537
2538 //-------------- TIMESERIES DATA ---------------//
2539 for (pugi::xml_node p = helios.child("timeseries"); p; p = p.next_sibling("timeseries")) {
2540 const char *label = p.attribute("label").value();
2541
2542 for (pugi::xml_node d = p.child("datapoint"); d; d = d.next_sibling("datapoint")) {
2543 Time time;
2544 pugi::xml_node time_node = d.child("time");
2545 const char *time_str = time_node.child_value();
2546 if (strlen(time_str) > 0) {
2547 int3 time_ = string2int3(time_str);
2548 if (time_.x < 0 || time_.x > 23) {
2549 helios_runtime_error("ERROR (Context::loadXML): Invalid hour of " + std::to_string(time_.x) + " given in timeseries. Hour must be positive and not greater than 23.");
2550 } else if (time_.y < 0 || time_.y > 59) {
2551 helios_runtime_error("ERROR (Context::loadXML): Invalid minute of " + std::to_string(time_.y) + " given in timeseries. Minute must be positive and not greater than 59.");
2552 } else if (time_.z < 0 || time_.z > 59) {
2553 helios_runtime_error("ERROR (Context::loadXML): Invalid second of " + std::to_string(time_.z) + " given in timeseries. Second must be positive and not greater than 59.");
2554 }
2555 time = make_Time(time_.x, time_.y, time_.z);
2556 } else {
2557 helios_runtime_error("ERROR (Context::loadXML): No time was specified for timeseries datapoint.");
2558 }
2559
2560 Date date;
2561 bool date_flag = false;
2562
2563 pugi::xml_node date_node = d.child("date");
2564 const char *date_str = date_node.child_value();
2565 if (strlen(date_str) > 0) {
2566 int3 date_ = string2int3(date_str);
2567 if (date_.x < 1 || date_.x > 31) {
2568 helios_runtime_error("ERROR (Context::loadXML): Invalid day of month " + std::to_string(date_.x) + " given in timeseries. Day must be greater than zero and not greater than 31.");
2569 } else if (date_.y < 1 || date_.y > 12) {
2570 helios_runtime_error("ERROR (Context::loadXML): Invalid month of " + std::to_string(date_.y) + " given in timeseries. Month must be greater than zero and not greater than 12.");
2571 } else if (date_.z < 1000 || date_.z > 10000) {
2572 helios_runtime_error("ERROR (Context::loadXML): Invalid year of " + std::to_string(date_.z) + " given in timeseries. Year should be in YYYY format.");
2573 }
2574 date = make_Date(date_.x, date_.y, date_.z);
2575 date_flag = true;
2576 }
2577
2578 pugi::xml_node Jdate_node = d.child("dateJulian");
2579 const char *Jdate_str = Jdate_node.child_value();
2580 if (strlen(Jdate_str) > 0) {
2581 int2 date_ = string2int2(Jdate_str);
2582 if (date_.x < 1 || date_.x > 366) {
2583 helios_runtime_error("ERROR (Context::loadXML): Invalid Julian day of year " + std::to_string(date_.x) + " given in timeseries. Julian day must be greater than zero and not greater than 366.");
2584 } else if (date_.y < 1000 || date_.y > 10000) {
2585 helios_runtime_error("ERROR (Context::loadXML): Invalid year of " + std::to_string(date_.y) + " given in timeseries. Year should be in YYYY format.");
2586 }
2587 date = Julian2Calendar(date_.x, date_.y);
2588 date_flag = true;
2589 }
2590
2591 if (!date_flag) {
2592 helios_runtime_error("ERROR (Context::loadXML): No date was specified for timeseries datapoint.");
2593 }
2594
2595 float value;
2596 pugi::xml_node value_node = d.child("value");
2597 const char *value_str = value_node.child_value();
2598 if (strlen(value_str) > 0) {
2599 if (!parse_float(value_str, value)) {
2600 helios_runtime_error("ERROR (Context::loadXML): Datapoint value in 'timeseries' block must be a float value.");
2601 }
2602 } else {
2603 helios_runtime_error("ERROR (Context::loadXML): No value was specified for timeseries datapoint.");
2604 }
2605
2606 addTimeseriesData(label, value, date, time);
2607 }
2608 }
2609
2610 if (!quiet) {
2611 std::cout << "done." << std::endl;
2612 }
2613
2614 return UUID;
2615}
2616
2617std::vector<std::string> Context::getLoadedXMLFiles() {
2618 return XMLfiles;
2619}
2620
2621bool Context::scanXMLForTag(const std::string &filename, const std::string &tag, const std::string &label) {
2622 const std::string &fn = filename;
2623 std::string ext = getFileExtension(filename);
2624 if (ext != ".xml" && ext != ".XML") {
2625 helios_runtime_error("failed.\n File " + fn + " is not XML format.");
2626 }
2627
2628 // Using "pugixml" parser. See pugixml.org
2629 pugi::xml_document xmldoc;
2630
2631 // load file
2632 pugi::xml_parse_result load_result = xmldoc.load_file(filename.c_str());
2633
2634 // error checking
2635 if (!load_result) {
2636 helios_runtime_error("failed.\n XML [" + filename + "] parsed with errors, attr value: [" + xmldoc.child("node").attribute("attr").value() + "]\nError description: " + load_result.description() +
2637 "\nError offset: " + std::to_string(load_result.offset) + " (error at [..." + (filename.c_str() + load_result.offset) + "]\n");
2638 }
2639
2640 pugi::xml_node helios = xmldoc.child("helios");
2641
2642 if (helios.empty()) {
2643 return false;
2644 }
2645
2646 for (pugi::xml_node p = helios.child(tag.c_str()); p; p = p.next_sibling(tag.c_str())) {
2647 const char *labelquery = p.attribute("label").value();
2648
2649 if (labelquery == label || label.empty()) {
2650 return true;
2651 }
2652 }
2653
2654 return false;
2655}
2656
2657void Context::writeDataToXMLstream(const char *data_group, const std::vector<std::string> &data_labels, void *ptr, std::ofstream &outfile) const {
2658 for (const auto &label: data_labels) {
2660
2661 if (strcmp(data_group, "primitive") == 0) {
2662 dtype = ((Primitive *) ptr)->getPrimitiveDataType(label.c_str());
2663 } else if (strcmp(data_group, "object") == 0) {
2664 dtype = ((CompoundObject *) ptr)->getObjectDataType(label.c_str());
2665 } else if (strcmp(data_group, "material") == 0) {
2666 dtype = ((Material *) ptr)->getMaterialDataType(label.c_str());
2667 } else if (strcmp(data_group, "global") == 0) {
2668 dtype = getGlobalDataType(label.c_str());
2669 } else {
2670 helios_runtime_error("ERROR (Context::writeDataToXMLstream): unknown data group argument of " + std::string(data_group) + ". Must be one of primitive, object, material, or global.");
2671 }
2672
2673 if (dtype == HELIOS_TYPE_UINT) {
2674 outfile << "\t<data_uint label=\"" << label << "\">" << std::flush;
2675 std::vector<uint> data;
2676 if (strcmp(data_group, "primitive") == 0) {
2677 ((Primitive *) ptr)->getPrimitiveData(label.c_str(), data);
2678 } else if (strcmp(data_group, "object") == 0) {
2679 ((CompoundObject *) ptr)->getObjectData(label.c_str(), data);
2680 } else if (strcmp(data_group, "material") == 0) {
2681 ((Material *) ptr)->getMaterialData(label.c_str(), data);
2682 } else {
2683 getGlobalData(label.c_str(), data);
2684 }
2685 for (int j = 0; j < data.size(); j++) {
2686 outfile << data.at(j) << std::flush;
2687 if (j != data.size() - 1) {
2688 outfile << " " << std::flush;
2689 }
2690 }
2691 outfile << "</data_uint>" << std::endl;
2692 } else if (dtype == HELIOS_TYPE_INT) {
2693 outfile << "\t<data_int label=\"" << label << "\">" << std::flush;
2694 std::vector<int> data;
2695 if (strcmp(data_group, "primitive") == 0) {
2696 ((Primitive *) ptr)->getPrimitiveData(label.c_str(), data);
2697 } else if (strcmp(data_group, "object") == 0) {
2698 ((CompoundObject *) ptr)->getObjectData(label.c_str(), data);
2699 } else if (strcmp(data_group, "material") == 0) {
2700 ((Material *) ptr)->getMaterialData(label.c_str(), data);
2701 } else {
2702 getGlobalData(label.c_str(), data);
2703 }
2704 for (int j = 0; j < data.size(); j++) {
2705 outfile << data.at(j) << std::flush;
2706 if (j != data.size() - 1) {
2707 outfile << " " << std::flush;
2708 }
2709 }
2710 outfile << "</data_int>" << std::endl;
2711 } else if (dtype == HELIOS_TYPE_FLOAT) {
2712 outfile << "\t<data_float label=\"" << label << "\">" << std::flush;
2713 std::vector<float> data;
2714 if (strcmp(data_group, "primitive") == 0) {
2715 ((Primitive *) ptr)->getPrimitiveData(label.c_str(), data);
2716 } else if (strcmp(data_group, "object") == 0) {
2717 ((CompoundObject *) ptr)->getObjectData(label.c_str(), data);
2718 } else if (strcmp(data_group, "material") == 0) {
2719 ((Material *) ptr)->getMaterialData(label.c_str(), data);
2720 } else {
2721 getGlobalData(label.c_str(), data);
2722 }
2723 for (int j = 0; j < data.size(); j++) {
2724 outfile << data.at(j) << std::flush;
2725 if (j != data.size() - 1) {
2726 outfile << " " << std::flush;
2727 }
2728 }
2729 outfile << "</data_float>" << std::endl;
2730 } else if (dtype == HELIOS_TYPE_DOUBLE) {
2731 outfile << "\t<data_double label=\"" << label << "\">" << std::flush;
2732 std::vector<double> data;
2733 if (strcmp(data_group, "primitive") == 0) {
2734 ((Primitive *) ptr)->getPrimitiveData(label.c_str(), data);
2735 } else if (strcmp(data_group, "object") == 0) {
2736 ((CompoundObject *) ptr)->getObjectData(label.c_str(), data);
2737 } else if (strcmp(data_group, "material") == 0) {
2738 ((Material *) ptr)->getMaterialData(label.c_str(), data);
2739 } else {
2740 getGlobalData(label.c_str(), data);
2741 }
2742 for (int j = 0; j < data.size(); j++) {
2743 outfile << data.at(j) << std::flush;
2744 if (j != data.size() - 1) {
2745 outfile << " " << std::flush;
2746 }
2747 }
2748 outfile << "</data_double>" << std::endl;
2749 } else if (dtype == HELIOS_TYPE_VEC2) {
2750 outfile << "\t<data_vec2 label=\"" << label << "\">" << std::flush;
2751 std::vector<vec2> data;
2752 if (strcmp(data_group, "primitive") == 0) {
2753 ((Primitive *) ptr)->getPrimitiveData(label.c_str(), data);
2754 } else if (strcmp(data_group, "object") == 0) {
2755 ((CompoundObject *) ptr)->getObjectData(label.c_str(), data);
2756 } else if (strcmp(data_group, "material") == 0) {
2757 ((Material *) ptr)->getMaterialData(label.c_str(), data);
2758 } else {
2759 getGlobalData(label.c_str(), data);
2760 }
2761 for (int j = 0; j < data.size(); j++) {
2762 outfile << data.at(j).x << " " << data.at(j).y << std::flush;
2763 if (j != data.size() - 1) {
2764 outfile << " " << std::flush;
2765 }
2766 }
2767 outfile << "</data_vec2>" << std::endl;
2768 } else if (dtype == HELIOS_TYPE_VEC3) {
2769 outfile << "\t<data_vec3 label=\"" << label << "\">" << std::flush;
2770 std::vector<vec3> data;
2771 if (strcmp(data_group, "primitive") == 0) {
2772 ((Primitive *) ptr)->getPrimitiveData(label.c_str(), data);
2773 } else if (strcmp(data_group, "object") == 0) {
2774 ((CompoundObject *) ptr)->getObjectData(label.c_str(), data);
2775 } else if (strcmp(data_group, "material") == 0) {
2776 ((Material *) ptr)->getMaterialData(label.c_str(), data);
2777 } else {
2778 getGlobalData(label.c_str(), data);
2779 }
2780 for (int j = 0; j < data.size(); j++) {
2781 outfile << data.at(j).x << " " << data.at(j).y << " " << data.at(j).z << std::flush;
2782 if (j != data.size() - 1) {
2783 outfile << " " << std::flush;
2784 }
2785 }
2786 outfile << "</data_vec3>" << std::endl;
2787 } else if (dtype == HELIOS_TYPE_VEC4) {
2788 outfile << "\t<data_vec4 label=\"" << label << "\">" << std::flush;
2789 std::vector<vec4> data;
2790 if (strcmp(data_group, "primitive") == 0) {
2791 ((Primitive *) ptr)->getPrimitiveData(label.c_str(), data);
2792 } else if (strcmp(data_group, "object") == 0) {
2793 ((CompoundObject *) ptr)->getObjectData(label.c_str(), data);
2794 } else if (strcmp(data_group, "material") == 0) {
2795 ((Material *) ptr)->getMaterialData(label.c_str(), data);
2796 } else {
2797 getGlobalData(label.c_str(), data);
2798 }
2799 for (int j = 0; j < data.size(); j++) {
2800 outfile << data.at(j).x << " " << data.at(j).y << " " << data.at(j).z << " " << data.at(j).w << std::flush;
2801 if (j != data.size() - 1) {
2802 outfile << " " << std::flush;
2803 }
2804 }
2805 outfile << "</data_vec4>" << std::endl;
2806 } else if (dtype == HELIOS_TYPE_INT2) {
2807 outfile << "\t<data_int2 label=\"" << label << "\">" << std::flush;
2808 std::vector<int2> data;
2809 if (strcmp(data_group, "primitive") == 0) {
2810 ((Primitive *) ptr)->getPrimitiveData(label.c_str(), data);
2811 } else if (strcmp(data_group, "object") == 0) {
2812 ((CompoundObject *) ptr)->getObjectData(label.c_str(), data);
2813 } else if (strcmp(data_group, "material") == 0) {
2814 ((Material *) ptr)->getMaterialData(label.c_str(), data);
2815 } else {
2816 getGlobalData(label.c_str(), data);
2817 }
2818 for (int j = 0; j < data.size(); j++) {
2819 outfile << data.at(j).x << " " << data.at(j).y << std::flush;
2820 if (j != data.size() - 1) {
2821 outfile << " " << std::flush;
2822 }
2823 }
2824 outfile << "</data_int2>" << std::endl;
2825 } else if (dtype == HELIOS_TYPE_INT3) {
2826 outfile << "\t<data_int3 label=\"" << label << "\">" << std::flush;
2827 std::vector<int3> data;
2828 if (strcmp(data_group, "primitive") == 0) {
2829 ((Primitive *) ptr)->getPrimitiveData(label.c_str(), data);
2830 } else if (strcmp(data_group, "object") == 0) {
2831 ((CompoundObject *) ptr)->getObjectData(label.c_str(), data);
2832 } else if (strcmp(data_group, "material") == 0) {
2833 ((Material *) ptr)->getMaterialData(label.c_str(), data);
2834 } else {
2835 getGlobalData(label.c_str(), data);
2836 }
2837 for (int j = 0; j < data.size(); j++) {
2838 outfile << data.at(j).x << " " << data.at(j).y << " " << data.at(j).z << std::flush;
2839 if (j != data.size() - 1) {
2840 outfile << " " << std::flush;
2841 }
2842 }
2843 outfile << "</data_int3>" << std::endl;
2844 } else if (dtype == HELIOS_TYPE_INT4) {
2845 outfile << "\t<data_int3 label=\"" << label << "\">" << std::flush;
2846 std::vector<int4> data;
2847 if (strcmp(data_group, "primitive") == 0) {
2848 ((Primitive *) ptr)->getPrimitiveData(label.c_str(), data);
2849 } else if (strcmp(data_group, "object") == 0) {
2850 ((CompoundObject *) ptr)->getObjectData(label.c_str(), data);
2851 } else if (strcmp(data_group, "material") == 0) {
2852 ((Material *) ptr)->getMaterialData(label.c_str(), data);
2853 } else {
2854 getGlobalData(label.c_str(), data);
2855 }
2856 for (int j = 0; j < data.size(); j++) {
2857 outfile << data.at(j).x << " " << data.at(j).y << " " << data.at(j).z << " " << data.at(j).w << std::flush;
2858 if (j != data.size() - 1) {
2859 outfile << " " << std::flush;
2860 }
2861 }
2862 outfile << "</data_int4>" << std::endl;
2863 } else if (dtype == HELIOS_TYPE_STRING) {
2864 outfile << "\t<data_string label=\"" << label << "\">" << std::flush;
2865 std::vector<std::string> data;
2866 if (strcmp(data_group, "primitive") == 0) {
2867 ((Primitive *) ptr)->getPrimitiveData(label.c_str(), data);
2868 } else if (strcmp(data_group, "object") == 0) {
2869 ((CompoundObject *) ptr)->getObjectData(label.c_str(), data);
2870 } else if (strcmp(data_group, "material") == 0) {
2871 ((Material *) ptr)->getMaterialData(label.c_str(), data);
2872 } else {
2873 getGlobalData(label.c_str(), data);
2874 }
2875 for (int j = 0; j < data.size(); j++) {
2876 outfile << data.at(j) << std::flush;
2877 if (j != data.size() - 1) {
2878 outfile << " " << std::flush;
2879 }
2880 }
2881 outfile << "</data_string>" << std::endl;
2882 }
2883 }
2884}
2885
2886void Context::writeXML(const char *filename, bool quiet) const {
2887 writeXML(filename, getAllUUIDs(), quiet);
2888}
2889
2890void Context::writeXML_byobject(const char *filename, const std::vector<uint> &objIDs, bool quiet) const {
2891 for (uint objID: objIDs) {
2892 if (!doesObjectExist(objID)) {
2893 helios_runtime_error("ERROR (Context::writeXML_byobject): Object with ID of " + std::to_string(objID) + " does not exist.");
2894 }
2895 }
2896 writeXML(filename, getObjectPrimitiveUUIDs(objIDs), quiet);
2897}
2898
2899void Context::writeXML(const char *filename, const std::vector<uint> &UUIDs, bool quiet) const {
2900 if (!quiet) {
2901 std::cout << "Writing XML file " << filename << "..." << std::flush;
2902 }
2903
2904 std::string xmlfilename = filename;
2905
2906 if (!validateOutputPath(xmlfilename)) {
2907 helios_runtime_error("ERROR (Context::writeXML): Invalid output file " + xmlfilename + ".");
2908 }
2909
2910 if (getFileName(xmlfilename).empty()) {
2911 helios_runtime_error("ERROR (Context::writeXML): Invalid output file " + xmlfilename + ". No file name was provided.");
2912 }
2913
2914 auto file_extension = getFileExtension(filename);
2915 if (file_extension != ".xml" && file_extension != ".XML") { // append xml to file name
2916 xmlfilename.append(".xml");
2917 }
2918
2919 std::vector<uint> objectIDs = getUniquePrimitiveParentObjectIDs(UUIDs, false);
2920
2921 std::ofstream outfile;
2922 outfile.open(xmlfilename);
2923
2924 outfile << "<?xml version=\"1.0\"?>\n\n";
2925
2926 outfile << "<helios>\n\n";
2927
2928 // -- materials -- //
2929
2930 // Collect unique material labels used by the primitives being written
2931 std::set<std::string> material_labels_used;
2932 for (uint UUID: UUIDs) {
2933 if (doesPrimitiveExist(UUID)) {
2934 uint matID = getPrimitivePointer_private(UUID)->materialID;
2935 if (materials.find(matID) != materials.end()) {
2936 material_labels_used.insert(materials.at(matID).label);
2937 }
2938 }
2939 }
2940
2941 if (!material_labels_used.empty()) {
2942 outfile << " <materials>" << std::endl;
2943 for (const std::string &label: material_labels_used) {
2944 if (doesMaterialExist(label)) {
2945 uint matID = getMaterialIDFromLabel(label);
2946 const Material &mat = materials.at(matID);
2947 outfile << "\t<material label=\"" << mat.label << "\">" << std::endl;
2948 outfile << "\t\t<color>" << mat.color.r << " " << mat.color.g << " " << mat.color.b << " " << mat.color.a << "</color>" << std::endl;
2949 if (!mat.texture_file.empty()) {
2950 outfile << "\t\t<texture>" << mat.texture_file << "</texture>" << std::endl;
2951 }
2952 if (mat.texture_color_overridden) {
2953 outfile << "\t\t<texture_override>1</texture_override>" << std::endl;
2954 }
2955 if (mat.twosided_flag != 1) { // Only write if non-default
2956 outfile << "\t\t<twosided_flag>" << mat.twosided_flag << "</twosided_flag>" << std::endl;
2957 }
2958 // Write material data
2959 std::vector<std::string> mdata = mat.listMaterialData();
2960 if (!mdata.empty()) {
2961 writeDataToXMLstream("material", mdata, const_cast<Material *>(&mat), outfile);
2962 }
2963 outfile << "\t</material>" << std::endl;
2964 }
2965 }
2966 outfile << " </materials>\n" << std::endl;
2967 }
2968
2969 // -- time/date -- //
2970
2971 Date date = getDate();
2972
2973 outfile << " <date>" << std::endl;
2974
2975 outfile << "\t<day>" << date.day << "</day>" << std::endl;
2976 outfile << "\t<month>" << date.month << "</month>" << std::endl;
2977 outfile << "\t<year>" << date.year << "</year>" << std::endl;
2978
2979 outfile << " </date>" << std::endl;
2980
2981 Time time = getTime();
2982
2983 outfile << " <time>" << std::endl;
2984
2985 outfile << "\t<hour>" << time.hour << "</hour>" << std::endl;
2986 outfile << "\t<minute>" << time.minute << "</minute>" << std::endl;
2987 outfile << "\t<second>" << time.second << "</second>" << std::endl;
2988
2989 outfile << " </time>" << std::endl;
2990
2991 // -- primitives -- //
2992
2993 for (uint UUID: UUIDs) {
2994 uint p = UUID;
2995
2996 if (!doesPrimitiveExist(p)) {
2997 if (doesObjectExist(p)) {
2998 helios_runtime_error("ERROR (Context::writeXML): Primitive with UUID of " + std::to_string(p) + " does not exist. There is a compound object with this ID - did you mean to call Context::writeXML_byobject()?");
2999 } else {
3000 helios_runtime_error("ERROR (Context::writeXML): Primitive with UUID of " + std::to_string(p) + " does not exist.");
3001 }
3002 }
3003
3004 Primitive *prim = getPrimitivePointer_private(p);
3005
3006 uint parent_objID = prim->getParentObjectID();
3007
3008 RGBAcolor color = prim->getColorRGBA();
3009
3010 std::string texture_file = prim->getTextureFile();
3011
3012 std::vector<std::string> pdata = prim->listPrimitiveData();
3013
3014 // if this primitive is a member of a compound object that is "complete", don't write it to XML
3015 //\todo This was included to make the XML files more efficient and avoid writing all object primitives to file. However, it doesn't work in some cases because it makes it hard to figure out the primitive transformations.
3016 // if( parent_objID>0 && areObjectPrimitivesComplete(parent_objID) ){
3017 // continue;
3018 // }
3019
3020 if (prim->getType() == PRIMITIVE_TYPE_PATCH) {
3021 outfile << " <patch>" << std::endl;
3022 } else if (prim->getType() == PRIMITIVE_TYPE_TRIANGLE) {
3023 outfile << " <triangle>" << std::endl;
3024 } else if (prim->getType() == PRIMITIVE_TYPE_VOXEL) {
3025 outfile << " <voxel>" << std::endl;
3026 }
3027
3028 outfile << "\t<UUID>" << p << "</UUID>" << std::endl;
3029
3030 if (parent_objID > 0) {
3031 outfile << "\t<objID>" << parent_objID << "</objID>" << std::endl;
3032 }
3033
3034 // Write material label (v3 format)
3035 if (materials.find(prim->materialID) != materials.end()) {
3036 outfile << "\t<material>" << materials.at(prim->materialID).label << "</material>" << std::endl;
3037 }
3038
3039 if (!pdata.empty()) {
3040 writeDataToXMLstream("primitive", pdata, prim, outfile);
3041 }
3042
3043 // Patches
3044 if (prim->getType() == PRIMITIVE_TYPE_PATCH) {
3045 Patch *patch = getPatchPointer_private(p);
3046 float transform[16];
3047 prim->getTransformationMatrix(transform);
3048
3049 outfile << "\t<transform>";
3050 for (float i: transform) {
3051 outfile << i << " ";
3052 }
3053 outfile << "</transform>" << std::endl;
3054 std::vector<vec2> uv = patch->getTextureUV();
3055 if (!uv.empty()) {
3056 outfile << "\t<textureUV>" << std::flush;
3057 for (int i = 0; i < uv.size(); i++) {
3058 outfile << uv.at(i).x << " " << uv.at(i).y << std::flush;
3059 if (i != uv.size() - 1) {
3060 outfile << " " << std::flush;
3061 }
3062 }
3063 outfile << "</textureUV>" << std::endl;
3064 }
3066 outfile << "\t<solid_fraction>" << getPrimitiveSolidFraction(p) << "</solid_fraction>\n";
3067 }
3068 outfile << " </patch>" << std::endl;
3069
3070 // Triangles
3071 } else if (prim->getType() == PRIMITIVE_TYPE_TRIANGLE) {
3072 float transform[16];
3073 prim->getTransformationMatrix(transform);
3074
3075 outfile << "\t<transform>";
3076 for (float i: transform) {
3077 outfile << i << " ";
3078 }
3079 outfile << "</transform>" << std::endl;
3080
3081 std::vector<vec2> uv = getTrianglePointer_private(p)->getTextureUV();
3082 if (!uv.empty()) {
3083 outfile << "\t<textureUV>" << std::flush;
3084 for (int i = 0; i < uv.size(); i++) {
3085 outfile << uv.at(i).x << " " << uv.at(i).y << std::flush;
3086 if (i != uv.size() - 1) {
3087 outfile << " " << std::flush;
3088 }
3089 }
3090 outfile << "</textureUV>" << std::endl;
3091 }
3093 outfile << "\t<solid_fraction>" << getPrimitiveSolidFraction(p) << "</solid_fraction>\n";
3094 }
3095 outfile << " </triangle>" << std::endl;
3096
3097 // Voxels
3098 } else if (prim->getType() == PRIMITIVE_TYPE_VOXEL) {
3099 float transform[16];
3100 prim->getTransformationMatrix(transform);
3101
3102 outfile << "\t<transform>";
3103 for (float i: transform) {
3104 outfile << i << " ";
3105 }
3106 outfile << "</transform>" << std::endl;
3108 outfile << "\t<solid_fraction>" << getPrimitiveSolidFraction(p) << "</solid_fraction>\n";
3109 }
3110
3111 outfile << " </voxel>" << std::endl;
3112 }
3113 }
3114
3115 // -- objects -- //
3116
3117 for (auto o: objectIDs) {
3118 CompoundObject *obj = objects.at(o);
3119
3120 std::string texture_file = obj->getTextureFile();
3121
3122 std::vector<std::string> odata = obj->listObjectData();
3123
3124 if (obj->getObjectType() == OBJECT_TYPE_TILE) {
3125 outfile << " <tile>" << std::endl;
3126 } else if (obj->getObjectType() == OBJECT_TYPE_BOX) {
3127 outfile << " <box>" << std::endl;
3128 } else if (obj->getObjectType() == OBJECT_TYPE_CONE) {
3129 outfile << " <cone>" << std::endl;
3130 } else if (obj->getObjectType() == OBJECT_TYPE_DISK) {
3131 outfile << " <disk>" << std::endl;
3132 } else if (obj->getObjectType() == OBJECT_TYPE_SPHERE) {
3133 outfile << " <sphere>" << std::endl;
3134 } else if (obj->getObjectType() == OBJECT_TYPE_TUBE) {
3135 outfile << " <tube>" << std::endl;
3136 } else if (obj->getObjectType() == OBJECT_TYPE_POLYMESH) {
3137 outfile << " <polymesh>" << std::endl;
3138 }
3139
3140 outfile << "\t<objID>" << o << "</objID>" << std::endl;
3141 if (obj->hasTexture()) {
3142 outfile << "\t<texture>" << texture_file << "</texture>" << std::endl;
3143 }
3144
3145 if (!odata.empty()) {
3146 writeDataToXMLstream("object", odata, obj, outfile);
3147 }
3148
3149 std::vector<std::string> pdata_labels;
3150 std::vector<HeliosDataType> pdata_types;
3151 std::vector<uint> primitiveUUIDs = obj->getPrimitiveUUIDs();
3152 for (uint UUID: primitiveUUIDs) {
3153 std::vector<std::string> labels = getPrimitivePointer_private(UUID)->listPrimitiveData();
3154 for (const auto &label: labels) {
3155 if (find(pdata_labels.begin(), pdata_labels.end(), label) == pdata_labels.end()) {
3156 pdata_labels.push_back(label);
3157 pdata_types.push_back(getPrimitiveDataType(label.c_str()));
3158 }
3159 }
3160 }
3161 for (size_t l = 0; l < pdata_labels.size(); l++) {
3162 if (pdata_types.at(l) == HELIOS_TYPE_FLOAT) {
3163 outfile << "\t<primitive_data_float " << "label=\"" << pdata_labels.at(l) << "\">" << std::endl;
3164 for (size_t p = 0; p < primitiveUUIDs.size(); p++) {
3165 if (doesPrimitiveDataExist(primitiveUUIDs.at(p), pdata_labels.at(l).c_str())) {
3166 std::vector<float> data;
3167 getPrimitiveData(primitiveUUIDs.at(p), pdata_labels.at(l).c_str(), data);
3168 outfile << "\t\t<data label=\"" << p << "\"> " << std::flush;
3169 for (float i: data) {
3170 outfile << i << std::flush;
3171 }
3172 outfile << " </data>" << std::endl;
3173 }
3174 }
3175 outfile << "\t</primitive_data_float>" << std::endl;
3176 } else if (pdata_types.at(l) == HELIOS_TYPE_DOUBLE) {
3177 outfile << "\t<primitive_data_double " << "label=\"" << pdata_labels.at(l) << "\">" << std::endl;
3178 for (size_t p = 0; p < primitiveUUIDs.size(); p++) {
3179 if (doesPrimitiveDataExist(primitiveUUIDs.at(p), pdata_labels.at(l).c_str())) {
3180 std::vector<double> data;
3181 getPrimitiveData(primitiveUUIDs.at(p), pdata_labels.at(l).c_str(), data);
3182 outfile << "\t\t<data label=\"" << p << "\"> " << std::flush;
3183 for (double i: data) {
3184 outfile << i << std::flush;
3185 }
3186 outfile << " </data>" << std::endl;
3187 }
3188 }
3189 outfile << "\t</primitive_data_double>" << std::endl;
3190 } else if (pdata_types.at(l) == HELIOS_TYPE_UINT) {
3191 outfile << "\t<primitive_data_uint " << "label=\"" << pdata_labels.at(l) << "\">" << std::endl;
3192 for (size_t p = 0; p < primitiveUUIDs.size(); p++) {
3193 if (doesPrimitiveDataExist(primitiveUUIDs.at(p), pdata_labels.at(l).c_str())) {
3194 std::vector<uint> data;
3195 getPrimitiveData(primitiveUUIDs.at(p), pdata_labels.at(l).c_str(), data);
3196 outfile << "\t\t<data label=\"" << p << "\"> " << std::flush;
3197 for (unsigned int i: data) {
3198 outfile << i << std::flush;
3199 }
3200 outfile << " </data>" << std::endl;
3201 }
3202 }
3203 outfile << "\t</primitive_data_uint>" << std::endl;
3204 } else if (pdata_types.at(l) == HELIOS_TYPE_INT) {
3205 outfile << "\t<primitive_data_int " << "label=\"" << pdata_labels.at(l) << "\">" << std::endl;
3206 for (size_t p = 0; p < primitiveUUIDs.size(); p++) {
3207 if (doesPrimitiveDataExist(primitiveUUIDs.at(p), pdata_labels.at(l).c_str())) {
3208 std::vector<int> data;
3209 getPrimitiveData(primitiveUUIDs.at(p), pdata_labels.at(l).c_str(), data);
3210 outfile << "\t\t<data label=\"" << p << "\"> " << std::flush;
3211 for (int i: data) {
3212 outfile << i << std::flush;
3213 }
3214 outfile << " </data>" << std::endl;
3215 }
3216 }
3217 outfile << "\t</primitive_data_int>" << std::endl;
3218 } else if (pdata_types.at(l) == HELIOS_TYPE_INT2) {
3219 outfile << "\t<primitive_data_int2 " << "label=\"" << pdata_labels.at(l) << "\">" << std::endl;
3220 for (size_t p = 0; p < primitiveUUIDs.size(); p++) {
3221 if (doesPrimitiveDataExist(primitiveUUIDs.at(p), pdata_labels.at(l).c_str())) {
3222 std::vector<int2> data;
3223 getPrimitiveData(primitiveUUIDs.at(p), pdata_labels.at(l).c_str(), data);
3224 outfile << "\t\t<data label=\"" << p << "\"> " << std::flush;
3225 for (auto &i: data) {
3226 outfile << i.x << " " << i.y << std::flush;
3227 }
3228 outfile << " </data>" << std::endl;
3229 }
3230 }
3231 outfile << "\t</primitive_data_int2>" << std::endl;
3232 } else if (pdata_types.at(l) == HELIOS_TYPE_INT3) {
3233 outfile << "\t<primitive_data_int3 " << "label=\"" << pdata_labels.at(l) << "\">" << std::endl;
3234 for (size_t p = 0; p < primitiveUUIDs.size(); p++) {
3235 if (doesPrimitiveDataExist(primitiveUUIDs.at(p), pdata_labels.at(l).c_str())) {
3236 std::vector<int3> data;
3237 getPrimitiveData(primitiveUUIDs.at(p), pdata_labels.at(l).c_str(), data);
3238 outfile << "\t\t<data label=\"" << p << "\"> " << std::flush;
3239 for (auto &i: data) {
3240 outfile << i.x << " " << i.y << " " << i.z << std::flush;
3241 }
3242 outfile << " </data>" << std::endl;
3243 }
3244 }
3245 outfile << "\t</primitive_data_int3>" << std::endl;
3246 } else if (pdata_types.at(l) == HELIOS_TYPE_INT4) {
3247 outfile << "\t<primitive_data_int4 " << "label=\"" << pdata_labels.at(l) << "\">" << std::endl;
3248 for (size_t p = 0; p < primitiveUUIDs.size(); p++) {
3249 if (doesPrimitiveDataExist(primitiveUUIDs.at(p), pdata_labels.at(l).c_str())) {
3250 std::vector<int4> data;
3251 getPrimitiveData(primitiveUUIDs.at(p), pdata_labels.at(l).c_str(), data);
3252 outfile << "\t\t<data label=\"" << p << "\"> " << std::flush;
3253 for (auto &i: data) {
3254 outfile << i.x << " " << i.y << " " << i.z << " " << i.w << std::flush;
3255 }
3256 outfile << " </data>" << std::endl;
3257 }
3258 }
3259 outfile << "\t</primitive_data_int4>" << std::endl;
3260 } else if (pdata_types.at(l) == HELIOS_TYPE_VEC2) {
3261 outfile << "\t<primitive_data_vec2 " << "label=\"" << pdata_labels.at(l) << "\">" << std::endl;
3262 for (size_t p = 0; p < primitiveUUIDs.size(); p++) {
3263 if (doesPrimitiveDataExist(primitiveUUIDs.at(p), pdata_labels.at(l).c_str())) {
3264 std::vector<vec2> data;
3265 getPrimitiveData(primitiveUUIDs.at(p), pdata_labels.at(l).c_str(), data);
3266 outfile << "\t\t<data label=\"" << p << "\"> " << std::flush;
3267 for (auto &i: data) {
3268 outfile << i.x << " " << i.y << std::flush;
3269 }
3270 outfile << " </data>" << std::endl;
3271 }
3272 }
3273 outfile << "\t</primitive_data_vec2>" << std::endl;
3274 } else if (pdata_types.at(l) == HELIOS_TYPE_VEC3) {
3275 outfile << "\t<primitive_data_vec3 " << "label=\"" << pdata_labels.at(l) << "\">" << std::endl;
3276 for (size_t p = 0; p < primitiveUUIDs.size(); p++) {
3277 if (doesPrimitiveDataExist(primitiveUUIDs.at(p), pdata_labels.at(l).c_str())) {
3278 std::vector<vec3> data;
3279 getPrimitiveData(primitiveUUIDs.at(p), pdata_labels.at(l).c_str(), data);
3280 outfile << "\t\t<data label=\"" << p << "\"> " << std::flush;
3281 for (auto &i: data) {
3282 outfile << i.x << " " << i.y << " " << i.z << std::flush;
3283 }
3284 outfile << " </data>" << std::endl;
3285 }
3286 }
3287 outfile << "\t</primitive_data_vec3>" << std::endl;
3288 } else if (pdata_types.at(l) == HELIOS_TYPE_VEC4) {
3289 outfile << "\t<primitive_data_vec4 " << "label=\"" << pdata_labels.at(l) << "\">" << std::endl;
3290 for (size_t p = 0; p < primitiveUUIDs.size(); p++) {
3291 if (doesPrimitiveDataExist(primitiveUUIDs.at(p), pdata_labels.at(l).c_str())) {
3292 std::vector<vec4> data;
3293 getPrimitiveData(primitiveUUIDs.at(p), pdata_labels.at(l).c_str(), data);
3294 outfile << "\t\t<data label=\"" << p << "\"> " << std::flush;
3295 for (auto &i: data) {
3296 outfile << i.x << " " << i.y << " " << i.z << " " << i.w << std::flush;
3297 }
3298 outfile << " </data>" << std::endl;
3299 }
3300 }
3301 outfile << "\t</primitive_data_vec4>" << std::endl;
3302 } else if (pdata_types.at(l) == HELIOS_TYPE_STRING) {
3303 outfile << "\t<primitive_data_string " << "label=\"" << pdata_labels.at(l) << "\">" << std::endl;
3304 for (size_t p = 0; p < primitiveUUIDs.size(); p++) {
3305 if (doesPrimitiveDataExist(primitiveUUIDs.at(p), pdata_labels.at(l).c_str())) {
3306 std::vector<std::string> data;
3307 getPrimitiveData(primitiveUUIDs.at(p), pdata_labels.at(l).c_str(), data);
3308 outfile << "\t\t<data label=\"" << p << "\"> " << std::flush;
3309 for (const auto &i: data) {
3310 outfile << i << std::flush;
3311 }
3312 outfile << " </data>" << std::endl;
3313 }
3314 }
3315 outfile << "\t</primitive_data_string>" << std::endl;
3316 }
3317 }
3318
3319 // Tiles
3320 if (obj->getObjectType() == OBJECT_TYPE_TILE) {
3321 Tile *tile = getTileObjectPointer_private(o);
3322
3323 float transform[16];
3324 tile->getTransformationMatrix(transform);
3325
3326 int2 subdiv = tile->getSubdivisionCount();
3327 outfile << "\t<subdivisions>" << subdiv.x << " " << subdiv.y << "</subdivisions>" << std::endl;
3328
3329 outfile << "\t<transform> ";
3330 for (float i: transform) {
3331 outfile << i << " ";
3332 }
3333 outfile << "</transform>" << std::endl;
3334
3335 outfile << " </tile>" << std::endl;
3336
3337 // Spheres
3338 } else if (obj->getObjectType() == OBJECT_TYPE_SPHERE) {
3339 Sphere *sphere = getSphereObjectPointer_private(o);
3340
3341 float transform[16];
3342 sphere->getTransformationMatrix(transform);
3343
3344 outfile << "\t<transform> ";
3345 for (float i: transform) {
3346 outfile << i << " ";
3347 }
3348 outfile << "</transform>" << std::endl;
3349
3350 uint subdiv = sphere->getSubdivisionCount();
3351 outfile << "\t<subdivisions> " << subdiv << " </subdivisions>" << std::endl;
3352
3353 outfile << " </sphere>" << std::endl;
3354
3355 // Tubes
3356 } else if (obj->getObjectType() == OBJECT_TYPE_TUBE) {
3357 Tube *tube = getTubeObjectPointer_private(o);
3358
3359 float transform[16];
3360 tube->getTransformationMatrix(transform);
3361
3362 outfile << "\t<transform> ";
3363 for (float i: transform) {
3364 outfile << i << " ";
3365 }
3366 outfile << "</transform>" << std::endl;
3367
3368 uint subdiv = tube->getSubdivisionCount();
3369 outfile << "\t<subdivisions> " << subdiv << " </subdivisions>" << std::endl;
3370
3371 std::vector<vec3> nodes = tube->getNodes();
3372 std::vector<float> radius = tube->getNodeRadii();
3373
3374 assert(nodes.size() == radius.size());
3375 outfile << "\t<nodes> " << std::endl;
3376 for (auto &node: nodes) {
3377 outfile << "\t\t" << node.x << " " << node.y << " " << node.z << std::endl;
3378 }
3379 outfile << "\t</nodes> " << std::endl;
3380 outfile << "\t<radius> " << std::endl;
3381 for (float radiu: radius) {
3382 outfile << "\t\t" << radiu << std::endl;
3383 }
3384 outfile << "\t</radius> " << std::endl;
3385
3386 if (texture_file.empty()) {
3387 std::vector<RGBcolor> colors = tube->getNodeColors();
3388
3389 outfile << "\t<color> " << std::endl;
3390 for (auto &color: colors) {
3391 outfile << "\t\t" << color.r << " " << color.g << " " << color.b << std::endl;
3392 }
3393 outfile << "\t</color> " << std::endl;
3394 }
3395
3396 outfile << " </tube>" << std::endl;
3397
3398 // Boxes
3399 } else if (obj->getObjectType() == OBJECT_TYPE_BOX) {
3400 Box *box = getBoxObjectPointer_private(o);
3401
3402 float transform[16];
3403 box->getTransformationMatrix(transform);
3404
3405 outfile << "\t<transform> ";
3406 for (float i: transform) {
3407 outfile << i << " ";
3408 }
3409 outfile << "</transform>" << std::endl;
3410
3411 int3 subdiv = box->getSubdivisionCount();
3412 outfile << "\t<subdivisions> " << subdiv.x << " " << subdiv.y << " " << subdiv.z << " </subdivisions>" << std::endl;
3413
3414 outfile << " </box>" << std::endl;
3415
3416 // Disks
3417 } else if (obj->getObjectType() == OBJECT_TYPE_DISK) {
3418 Disk *disk = getDiskObjectPointer_private(o);
3419
3420 float transform[16];
3421 disk->getTransformationMatrix(transform);
3422
3423 outfile << "\t<transform> ";
3424 for (float i: transform) {
3425 outfile << i << " ";
3426 }
3427 outfile << "</transform>" << std::endl;
3428
3429 int2 subdiv = disk->getSubdivisionCount();
3430 outfile << "\t<subdivisions> " << subdiv.x << " " << subdiv.y << " </subdivisions>" << std::endl;
3431
3432 outfile << " </disk>" << std::endl;
3433
3434 // Cones
3435 } else if (obj->getObjectType() == OBJECT_TYPE_CONE) {
3436 Cone *cone = getConeObjectPointer_private(o);
3437
3438 float transform[16];
3439 cone->getTransformationMatrix(transform);
3440
3441 outfile << "\t<transform> ";
3442 for (float i: transform) {
3443 outfile << i << " ";
3444 }
3445 outfile << "</transform>" << std::endl;
3446
3447 uint subdiv = cone->getSubdivisionCount();
3448 outfile << "\t<subdivisions> " << subdiv << " </subdivisions>" << std::endl;
3449
3450 std::vector<vec3> nodes = cone->getNodeCoordinates();
3451 std::vector<float> radius = cone->getNodeRadii();
3452
3453 assert(nodes.size() == radius.size());
3454 outfile << "\t<nodes> " << std::endl;
3455 for (auto &node: nodes) {
3456 outfile << "\t\t" << node.x << " " << node.y << " " << node.z << std::endl;
3457 }
3458 outfile << "\t</nodes> " << std::endl;
3459 outfile << "\t<radius> " << std::endl;
3460 for (float radiu: radius) {
3461 outfile << "\t\t" << radiu << std::endl;
3462 }
3463 outfile << "\t</radius> " << std::endl;
3464
3465 outfile << " </cone>" << std::endl;
3466
3467 // Polymesh
3468 } else if (obj->getObjectType() == OBJECT_TYPE_POLYMESH) {
3469 outfile << " </polymesh>" << std::endl;
3470 }
3471 }
3472
3473
3474 // -- global data -- //
3475
3476 for (const auto &iter: globaldata) {
3477 std::string label = iter.first;
3478 GlobalData data = iter.second;
3479 HeliosDataType type = data.type;
3480 if (type == HELIOS_TYPE_UINT) {
3481 outfile << " <globaldata_uint label=\"" << label << "\">" << std::flush;
3482 for (size_t i = 0; i < data.size; i++) {
3483 outfile << data.global_data_uint.at(i) << std::flush;
3484 if (i != data.size - 1) {
3485 outfile << " " << std::flush;
3486 }
3487 }
3488 outfile << "</globaldata_uint>" << std::endl;
3489 } else if (type == HELIOS_TYPE_INT) {
3490 outfile << " <globaldata_int label=\"" << label << "\">" << std::flush;
3491 for (size_t i = 0; i < data.size; i++) {
3492 outfile << data.global_data_int.at(i) << std::flush;
3493 if (i != data.size - 1) {
3494 outfile << " " << std::flush;
3495 }
3496 }
3497 outfile << "</globaldata_int>" << std::endl;
3498 } else if (type == HELIOS_TYPE_FLOAT) {
3499 outfile << " <globaldata_float label=\"" << label << "\">" << std::flush;
3500 for (size_t i = 0; i < data.size; i++) {
3501 outfile << data.global_data_float.at(i) << std::flush;
3502 if (i != data.size - 1) {
3503 outfile << " " << std::flush;
3504 }
3505 }
3506 outfile << "</globaldata_float>" << std::endl;
3507 } else if (type == HELIOS_TYPE_DOUBLE) {
3508 outfile << " <globaldata_double label=\"" << label << "\">" << std::flush;
3509 for (size_t i = 0; i < data.size; i++) {
3510 outfile << data.global_data_double.at(i) << std::flush;
3511 if (i != data.size - 1) {
3512 outfile << " " << std::flush;
3513 }
3514 }
3515 outfile << "</globaldata_double>" << std::endl;
3516 } else if (type == HELIOS_TYPE_VEC2) {
3517 outfile << " <globaldata_vec2 label=\"" << label << "\">" << std::endl;
3518 for (size_t i = 0; i < data.size; i++) {
3519 outfile << " " << data.global_data_vec2.at(i).x << " " << data.global_data_vec2.at(i).y << std::endl;
3520 }
3521 outfile << " </globaldata_vec2>" << std::endl;
3522 } else if (type == HELIOS_TYPE_VEC3) {
3523 outfile << " <globaldata_vec3 label=\"" << label << "\">" << std::endl;
3524 for (size_t i = 0; i < data.size; i++) {
3525 outfile << " " << data.global_data_vec3.at(i).x << " " << data.global_data_vec3.at(i).y << " " << data.global_data_vec3.at(i).z << std::endl;
3526 }
3527 outfile << " </globaldata_vec3>" << std::endl;
3528 } else if (type == HELIOS_TYPE_VEC4) {
3529 outfile << " <globaldata_vec4 label=\"" << label << "\">" << std::endl;
3530 for (size_t i = 0; i < data.size; i++) {
3531 outfile << " " << data.global_data_vec4.at(i).x << " " << data.global_data_vec4.at(i).y << " " << data.global_data_vec4.at(i).z << " " << data.global_data_vec4.at(i).w << std::endl;
3532 }
3533 outfile << " </globaldata_vec4>" << std::endl;
3534 } else if (type == HELIOS_TYPE_INT2) {
3535 outfile << " <globaldata_int2 label=\"" << label << "\">" << std::endl;
3536 for (size_t i = 0; i < data.size; i++) {
3537 outfile << " " << data.global_data_int2.at(i).x << " " << data.global_data_int2.at(i).y << std::endl;
3538 }
3539 outfile << " </globaldata_int2>" << std::endl;
3540 } else if (type == HELIOS_TYPE_INT3) {
3541 outfile << " <globaldata_int3 label=\"" << label << "\">" << std::endl;
3542 for (size_t i = 0; i < data.size; i++) {
3543 outfile << " " << data.global_data_int3.at(i).x << " " << data.global_data_int3.at(i).y << " " << data.global_data_int3.at(i).z << std::endl;
3544 }
3545 outfile << " </globaldata_int3>" << std::endl;
3546 } else if (type == HELIOS_TYPE_INT4) {
3547 outfile << " <globaldata_int4 label=\"" << label << "\">" << std::endl;
3548 for (size_t i = 0; i < data.size; i++) {
3549 outfile << " " << data.global_data_int4.at(i).x << " " << data.global_data_int4.at(i).y << " " << data.global_data_int4.at(i).z << " " << data.global_data_int4.at(i).w << std::endl;
3550 }
3551 outfile << " </globaldata_int4>" << std::endl;
3552 } else if (type == HELIOS_TYPE_STRING) {
3553 outfile << " <globaldata_string label=\"" << label << "\">" << std::flush;
3554 for (size_t i = 0; i < data.size; i++) {
3555 outfile << data.global_data_string.at(i) << std::flush;
3556 if (i != data.size - 1) {
3557 outfile << " " << std::flush;
3558 }
3559 }
3560 outfile << "</globaldata_string>" << std::endl;
3561 }
3562 }
3563
3564 // -- timeseries -- //
3565
3566 for (const auto &iter: timeseries_data) {
3567 std::string label = iter.first;
3568
3569 std::vector<float> data = iter.second;
3570 std::vector<double> dateval = timeseries_datevalue.at(label);
3571
3572 assert(data.size() == dateval.size());
3573
3574 outfile << " <timeseries label=\"" << label << "\">" << std::endl;
3575
3576 for (size_t i = 0; i < data.size(); i++) {
3577 Date a_date = queryTimeseriesDate(label.c_str(), i);
3578 Time a_time = queryTimeseriesTime(label.c_str(), i);
3579
3580 outfile << "\t<datapoint>" << std::endl;
3581
3582 outfile << "\t <date>" << a_date.day << " " << a_date.month << " " << a_date.year << "</date>" << std::endl;
3583
3584 outfile << "\t <time>" << a_time.hour << " " << a_time.minute << " " << a_time.second << "</time>" << std::endl;
3585
3586 outfile << "\t <value>" << data.at(i) << "</value>" << std::endl;
3587
3588 outfile << "\t</datapoint>" << std::endl;
3589 }
3590
3591 outfile << " </timeseries>" << std::endl;
3592 }
3593
3594 // ----------------- //
3595
3596 outfile << "\n</helios>\n";
3597
3598 outfile.close();
3599
3600 if (!quiet) {
3601 std::cout << "done." << std::endl;
3602 }
3603}
3604
3605std::vector<uint> Context::loadPLY(const char *filename, bool silent) {
3606 return loadPLY(filename, nullorigin, 0, nullrotation, RGB::blue, "YUP", silent);
3607}
3608
3609std::vector<uint> Context::loadPLY(const char *filename, const vec3 &origin, float height, const std::string &upaxis, bool silent) {
3610 return loadPLY(filename, origin, height, make_SphericalCoord(0, 0), RGB::blue, upaxis, silent);
3611}
3612
3613std::vector<uint> Context::loadPLY(const char *filename, const vec3 &origin, float height, const SphericalCoord &rotation, const std::string &upaxis, bool silent) {
3614 return loadPLY(filename, origin, height, rotation, RGB::blue, upaxis, silent);
3615}
3616
3617std::vector<uint> Context::loadPLY(const char *filename, const vec3 &origin, float height, const RGBcolor &default_color, const std::string &upaxis, bool silent) {
3618 return loadPLY(filename, origin, height, make_SphericalCoord(0, 0), default_color, upaxis, silent);
3619}
3620
3621std::vector<uint> Context::loadPLY(const char *filename, const vec3 &origin, float height, const SphericalCoord &rotation, const RGBcolor &default_color, const std::string &upaxis, bool silent) {
3622 if (!silent) {
3623 std::cout << "Reading PLY file " << filename << "..." << std::flush;
3624 }
3625
3626 std::string fn = filename;
3627 std::string ext = getFileExtension(filename);
3628 if (ext != ".ply" && ext != ".PLY") {
3629 helios_runtime_error("ERROR (Context::loadPLY): File " + fn + " is not PLY format.");
3630 }
3631
3632 if (upaxis != "XUP" && upaxis != "YUP" && upaxis != "ZUP") {
3633 helios_runtime_error("ERROR (Context::loadPLY): " + upaxis + " is not a valid up-axis. Please specify a value of XUP, YUP, or ZUP.");
3634 }
3635
3636 std::string line, prop;
3637
3638 uint vertexCount = 0, faceCount = 0;
3639
3640 std::vector<vec3> vertices;
3641 std::vector<std::vector<int>> faces;
3642 std::vector<RGBcolor> colors;
3643 std::vector<std::string> properties;
3644
3645 bool ifColor = false;
3646
3647 // Resolve file path using unified resolution
3648 std::filesystem::path resolved_path = resolveFilePath(filename);
3649 std::string resolved_filename = resolved_path.string();
3650
3651 std::ifstream inputPly;
3652 inputPly.open(resolved_filename);
3653
3654 if (!inputPly.is_open()) {
3655 helios_runtime_error("ERROR (Context::loadPLY): Couldn't open " + std::string(filename));
3656 }
3657
3658 //--- read header info -----//
3659
3660 // first line should always be 'ply'
3661 inputPly >> line;
3662 if ("ply" != line) {
3663 helios_runtime_error("ERROR (Context::loadPLY): " + std::string(filename) + " is not a PLY file.");
3664 }
3665
3666 // read format
3667 inputPly >> line;
3668 if ("format" != line) {
3669 helios_runtime_error("ERROR (Context::loadPLY): could not determine data format of " + std::string(filename));
3670 }
3671
3672 inputPly >> line;
3673 if ("ascii" != line) {
3674 helios_runtime_error("ERROR (Context::loadPLY): Only ASCII data types are supported.");
3675 }
3676
3677 std::string temp_string;
3678
3679 while ("end_header" != line) {
3680 inputPly >> line;
3681
3682 if ("comment" == line) {
3683 getline(inputPly, line);
3684 } else if ("element" == line) {
3685 inputPly >> line;
3686
3687 if ("vertex" == line) {
3688 inputPly >> temp_string;
3689 if (!parse_uint(temp_string, vertexCount)) {
3690 helios_runtime_error("ERROR (Context::loadPLY): PLY file read failed. Vertex count value should be a non-negative integer.");
3691 }
3692 } else if ("face" == line) {
3693 inputPly >> temp_string;
3694 if (!parse_uint(temp_string, faceCount)) {
3695 helios_runtime_error("ERROR (Context::loadPLY): PLY file read failed. Face count value should be a non-negative integer.");
3696 }
3697 }
3698 } else if ("property" == line) {
3699 inputPly >> line; // type
3700
3701 if ("list" != line) {
3702 inputPly >> prop; // value
3703 properties.push_back(prop);
3704 }
3705 }
3706 }
3707
3708 for (auto &property: properties) {
3709 if (property == "red") {
3710 ifColor = true;
3711 }
3712 }
3713 if (!silent) {
3714 std::cout << "forming " << faceCount << " triangles..." << std::flush;
3715 }
3716
3717 vertices.resize(vertexCount);
3718 colors.resize(vertexCount);
3719 faces.resize(faceCount);
3720
3721
3722 //--- read vertices ----//
3723
3724 for (uint row = 0; row < vertexCount; row++) {
3725 for (auto &property: properties) {
3726 if (property == "x") {
3727 inputPly >> temp_string;
3728 float x;
3729 if (!parse_float(temp_string, x)) {
3730 helios_runtime_error("ERROR (Context::loadPLY): X value for vertex " + std::to_string(row) + " is invalid and could not be read.");
3731 }
3732 if (upaxis == "XUP") {
3733 vertices.at(row).z = x;
3734 } else if (upaxis == "YUP") {
3735 vertices.at(row).y = x;
3736 } else if (upaxis == "ZUP") {
3737 vertices.at(row).x = x;
3738 }
3739 } else if (property == "y") {
3740 inputPly >> temp_string;
3741 float y;
3742 if (!parse_float(temp_string, y)) {
3743 helios_runtime_error("ERROR (Context::loadPLY): Y value for vertex " + std::to_string(row) + " is invalid and could not be read.");
3744 }
3745 if (upaxis == "XUP") {
3746 vertices.at(row).x = y;
3747 } else if (upaxis == "YUP") {
3748 vertices.at(row).z = y;
3749 } else if (upaxis == "ZUP") {
3750 vertices.at(row).y = y;
3751 }
3752 } else if (property == "z") {
3753 inputPly >> temp_string;
3754 float z;
3755 if (!parse_float(temp_string, z)) {
3756 helios_runtime_error("ERROR (Context::loadPLY): Z value for vertex " + std::to_string(row) + " is invalid and could not be read.");
3757 }
3758 if (upaxis == "XUP") {
3759 vertices.at(row).y = z;
3760 } else if (upaxis == "YUP") {
3761 vertices.at(row).x = z;
3762 } else if (upaxis == "ZUP") {
3763 vertices.at(row).z = z;
3764 }
3765 } else if (property == "red") {
3766 inputPly >> temp_string;
3767 if (!parse_float(temp_string, colors.at(row).r)) {
3768 helios_runtime_error("ERROR (Context::loadPLY): red color value for vertex " + std::to_string(row) + " is invalid and could not be read.");
3769 }
3770 colors.at(row).r /= 255.f;
3771 } else if (property == "green") {
3772 inputPly >> temp_string;
3773 if (!parse_float(temp_string, colors.at(row).g)) {
3774 helios_runtime_error("ERROR (Context::loadPLY): green color value for vertex " + std::to_string(row) + " is invalid and could not be read.");
3775 }
3776 colors.at(row).g /= 255.f;
3777 } else if (property == "blue") {
3778 inputPly >> temp_string;
3779 if (!parse_float(temp_string, colors.at(row).b)) {
3780 helios_runtime_error("ERROR (Context::loadPLY): blue color value for vertex " + std::to_string(row) + " is invalid and could not be read.");
3781 }
3782 colors.at(row).b /= 255.f;
3783 } else {
3784 inputPly >> line;
3785 }
3786 }
3787
3788 if (inputPly.eof()) {
3789 helios_runtime_error("ERROR (Context::loadPLY): Read past end of file while reading vertices. Vertex count specified in header may be incorrect.");
3790 }
3791 }
3792
3793 // determine bounding box
3794
3795 vec3 boxmin = make_vec3(10000, 10000, 10000);
3796 vec3 boxmax = make_vec3(-10000, -10000, -10000);
3797
3798 for (uint row = 0; row < vertexCount; row++) {
3799 if (vertices.at(row).x < boxmin.x) {
3800 boxmin.x = vertices.at(row).x;
3801 }
3802 if (vertices.at(row).y < boxmin.y) {
3803 boxmin.y = vertices.at(row).y;
3804 }
3805 if (vertices.at(row).z < boxmin.z) {
3806 boxmin.z = vertices.at(row).z;
3807 }
3808
3809 if (vertices.at(row).x > boxmax.x) {
3810 boxmax.x = vertices.at(row).x;
3811 }
3812 if (vertices.at(row).y > boxmax.y) {
3813 boxmax.y = vertices.at(row).y;
3814 }
3815 if (vertices.at(row).z > boxmax.z) {
3816 boxmax.z = vertices.at(row).z;
3817 }
3818 }
3819
3820 // center PLY object at `origin' and scale to have height `height'
3821 float scl = 1.f;
3822 if (height > 0.f) {
3823 scl = height / (boxmax.z - boxmin.z);
3824 }
3825 for (uint row = 0; row < vertexCount; row++) {
3826 vertices.at(row).z -= boxmin.z;
3827
3828 vertices.at(row).x *= scl;
3829 vertices.at(row).y *= scl;
3830 vertices.at(row).z *= scl;
3831
3832 vertices.at(row) = rotatePoint(vertices.at(row), rotation) + origin;
3833 }
3834
3835 //--- read faces ----//
3836
3837 uint v, ID;
3838 std::vector<uint> UUID;
3839 for (uint row = 0; row < faceCount; row++) {
3840 inputPly >> temp_string;
3841
3842 if (!parse_uint(temp_string, v)) {
3843 helios_runtime_error("ERROR (Context::loadPLY): Vertex count for face " + std::to_string(row) + " should be a non-negative integer.");
3844 }
3845
3846 faces.at(row).resize(v);
3847
3848 for (uint i = 0; i < v; i++) {
3849 inputPly >> temp_string;
3850 if (!parse_int(temp_string, faces.at(row).at(i))) {
3851 helios_runtime_error("ERROR (Context::loadPLY): Vertex index for face " + std::to_string(row) + " is invalid and could not be read.");
3852 }
3853 }
3854
3855 // Add triangles to context
3856
3857 for (uint t = 2; t < v; t++) {
3858 RGBcolor color;
3859 if (ifColor) {
3860 color = colors.at(faces.at(row).front());
3861 } else {
3862 color = default_color;
3863 }
3864
3865 vec3 v0 = vertices.at(faces.at(row).front());
3866 vec3 v1 = vertices.at(faces.at(row).at(t - 1));
3867 vec3 v2 = vertices.at(faces.at(row).at(t));
3868
3869 if ((v0 - v1).magnitude() < 1e-10f || (v0 - v2).magnitude() < 1e-10f || (v1 - v2).magnitude() < 1e-10f) {
3870 continue;
3871 }
3872
3873 // Additional check for triangle area to avoid near-degenerate triangles
3874 float triangle_area = calculateTriangleArea(v0, v1, v2);
3875 if (triangle_area < MIN_TRIANGLE_AREA_THRESHOLD) {
3876 continue;
3877 }
3878
3879 ID = addTriangle(v0, v1, v2, color);
3880
3881 UUID.push_back(ID);
3882 }
3883
3884 if (inputPly.eof()) {
3885 helios_runtime_error("ERROR (Context::loadPLY): Read past end of file while reading faces. Face count specified in header may be incorrect.");
3886 }
3887 }
3888
3889 if (!silent) {
3890 std::cout << "done." << std::endl;
3891 }
3892
3893 return UUID;
3894}
3895
3896void Context::writePLY(const char *filename) const {
3897 writePLY(filename, getAllUUIDs());
3898}
3899
3900void Context::writePLY(const char *filename, const std::vector<uint> &UUIDs) const {
3901 // Validate file name / extension
3902 std::string fname{filename ? filename : ""};
3903
3904 const auto dotPos = fname.find_last_of('.');
3905 const std::string ext = (dotPos != std::string::npos) ? fname.substr(dotPos) : "";
3906
3907 auto ciEqual = [](const char a, const char b) { return std::tolower(a) == std::tolower(b); };
3908 bool isPly = (ext.size() == 4) && ciEqual(ext[1], 'p') && ciEqual(ext[2], 'l') && ciEqual(ext[3], 'y');
3909
3910 if (!isPly) {
3911 helios_runtime_error("ERROR (Context::writePLY) Invalid file extension for " + fname + ". Expected a file ending in '.ply'.");
3912 }
3913
3914 // Try to open the output file
3915 std::ofstream PLYfile;
3916 PLYfile.open(fname, std::ios::out | std::ios::trunc);
3917
3918 if (!PLYfile.is_open()) {
3919 helios_runtime_error("ERROR (Context::writePLY) Unable to open " + fname + " for writing.");
3920 }
3921
3922 PLYfile << "ply" << std::endl << "format ascii 1.0" << std::endl << "comment Helios generated" << std::endl;
3923
3924 std::vector<int3> faces;
3925 std::vector<vec3> verts;
3926 std::vector<RGBcolor> colors;
3927
3928 size_t vertex_count = 0;
3929
3930 for (auto UUID: UUIDs) {
3931 std::vector<vec3> vertices = getPrimitivePointer_private(UUID)->getVertices();
3932 PrimitiveType type = getPrimitivePointer_private(UUID)->getType();
3933 RGBcolor C = getPrimitivePointer_private(UUID)->getColor();
3934 C.scale(255.f);
3935
3936 if (type == PRIMITIVE_TYPE_TRIANGLE) {
3937 faces.push_back(make_int3((int) vertex_count, (int) vertex_count + 1, (int) vertex_count + 2));
3938 for (int i = 0; i < 3; i++) {
3939 verts.push_back(vertices.at(i));
3940 colors.push_back(C);
3941 vertex_count++;
3942 }
3943 } else if (type == PRIMITIVE_TYPE_PATCH) {
3944 faces.push_back(make_int3((int) vertex_count, (int) vertex_count + 1, (int) vertex_count + 2));
3945 faces.push_back(make_int3((int) vertex_count, (int) vertex_count + 2, (int) vertex_count + 3));
3946 for (int i = 0; i < 4; i++) {
3947 verts.push_back(vertices.at(i));
3948 colors.push_back(C);
3949 vertex_count++;
3950 }
3951 }
3952 }
3953
3954 PLYfile << "element vertex " << verts.size() << std::endl;
3955 PLYfile << "property float x" << std::endl << "property float y" << std::endl << "property float z" << std::endl;
3956 PLYfile << "property uchar red" << std::endl << "property uchar green" << std::endl << "property uchar blue" << std::endl;
3957 PLYfile << "element face " << faces.size() << std::endl;
3958 PLYfile << "property list uchar int vertex_indices" << std::endl << "end_header" << std::endl;
3959
3960 for (size_t v = 0; v < verts.size(); v++) {
3961 PLYfile << verts.at(v).x << " " << verts.at(v).y << " " << verts.at(v).z << " " << round(colors.at(v).r) << " " << round(colors.at(v).g) << " " << round(colors.at(v).b) << std::endl;
3962 }
3963
3964 for (auto &face: faces) {
3965 PLYfile << "3 " << face.x << " " << face.y << " " << face.z << std::endl;
3966 }
3967
3968 PLYfile.close();
3969}
3970
3971std::vector<uint> Context::loadOBJ(const char *filename, bool silent) {
3972 return loadOBJ(filename, nullorigin, 0, nullrotation, RGB::blue, "ZUP", silent);
3973}
3974
3975std::vector<uint> Context::loadOBJ(const char *filename, const vec3 &origin, float height, const SphericalCoord &rotation, const RGBcolor &default_color, bool silent) {
3976 return loadOBJ(filename, origin, make_vec3(0, 0, height), rotation, default_color, "ZUP", silent);
3977}
3978
3979std::vector<uint> Context::loadOBJ(const char *filename, const vec3 &origin, float height, const SphericalCoord &rotation, const RGBcolor &default_color, const char *upaxis, bool silent) {
3980 return loadOBJ(filename, origin, make_vec3(0, 0, height), rotation, default_color, upaxis, silent);
3981}
3982
3983std::vector<uint> Context::loadOBJ(const char *filename, const vec3 &origin, const helios::vec3 &scale, const SphericalCoord &rotation, const RGBcolor &default_color, const char *upaxis, bool silent) {
3984
3985 if (!silent) {
3986 std::cout << "Reading OBJ file " << filename << "..." << std::flush;
3987 }
3988
3989 std::string fn = filename;
3990 std::string ext = getFileExtension(filename);
3991 if (ext != ".obj" && ext != ".OBJ") {
3992 helios_runtime_error("ERROR (Context::loadOBJ): File " + fn + " is not OBJ format.");
3993 }
3994
3995 if (strcmp(upaxis, "XUP") != 0 && strcmp(upaxis, "YUP") != 0 && strcmp(upaxis, "ZUP") != 0) {
3996 helios_runtime_error("ERROR (Context::loadOBJ): Up axis of " + std::string(upaxis) + " is not valid. Should be one of 'XUP', 'YUP', or 'ZUP'.");
3997 }
3998
3999 std::string line, prop;
4000
4001 std::vector<vec3> vertices;
4002 std::vector<std::string> objects;
4003 std::vector<vec2> texture_uv;
4004 std::map<std::string, std::vector<std::vector<int>>> face_inds, texture_inds;
4005
4006 std::map<std::string, OBJmaterial> materials;
4007
4008 std::vector<uint> UUID;
4009
4010 // Resolve file path using unified resolution
4011 std::filesystem::path resolved_path = resolveFilePath(filename);
4012 std::string resolved_filename = resolved_path.string();
4013
4014 std::ifstream inputOBJ, inputMTL;
4015 inputOBJ.open(resolved_filename);
4016
4017 if (!inputOBJ.is_open()) {
4018 helios_runtime_error("ERROR (Context::loadOBJ): Couldn't open " + std::string(filename));
4019 }
4020
4021 // determine the base file path for resolved filename
4022 std::string filebase = getFilePath(resolved_filename);
4023
4024 // determine bounding box
4025 float boxmin = 100000;
4026 float boxmax = -100000;
4027
4028 std::string current_material = "none";
4029 std::string current_object = "none";
4030
4031 size_t lineno = 0;
4032 while (inputOBJ.good()) {
4033 lineno++;
4034
4035 inputOBJ >> line;
4036
4037 // ------- COMMENTS --------- //
4038 if (line == "#") {
4039 getline(inputOBJ, line);
4040
4041 // ------- MATERIAL LIBRARY ------- //
4042 } else if (line == "mtllib") {
4043 getline(inputOBJ, line);
4044 std::string material_file = trim_whitespace(line);
4045 materials = loadMTL(filebase, material_file, default_color);
4046
4047 // ------- OBJECT ------- //
4048 } else if (line == "o") {
4049 getline(inputOBJ, line);
4050 current_object = trim_whitespace(line);
4051
4052 // ------- VERTICES --------- //
4053 } else if (line == "v") {
4054 getline(inputOBJ, line);
4055 // parse vertices into points
4056 vec3 verts(string2vec3(line.c_str()));
4057 vertices.emplace_back(verts);
4058 objects.emplace_back(current_object);
4059
4060 if (verts.z < boxmin) {
4061 boxmin = verts.z;
4062 }
4063 if (verts.z > boxmax) {
4064 boxmax = verts.z;
4065 }
4066
4067 // ------- TEXTURE COORDINATES --------- //
4068 } else if (line == "vt") {
4069 getline(inputOBJ, line);
4070 line = trim_whitespace(line);
4071 // parse coordinates into uv
4072 vec2 uv(string2vec2(line.c_str()));
4073 texture_uv.emplace_back(uv);
4074
4075 // ------- MATERIALS --------- //
4076 } else if (line == "usemtl") {
4077 getline(inputOBJ, line);
4078 current_material = trim_whitespace(line);
4079
4080 // ------- FACES --------- //
4081 } else if (line == "f") {
4082 getline(inputOBJ, line);
4083 // parse face vertices
4084 std::istringstream stream(line);
4085 std::string tmp, digitf, digitu;
4086 std::vector<int> f, u;
4087 while (stream.good()) {
4088 stream >> tmp;
4089
4090 digitf = "";
4091 int ic = 0;
4092 for (char i: tmp) {
4093 if (isdigit(i)) {
4094 digitf.push_back(i);
4095 ic++;
4096 } else {
4097 break;
4098 }
4099 }
4100
4101 digitu = "";
4102 for (int i = ic + 1; i < tmp.size(); i++) {
4103 if (isdigit(tmp[i])) {
4104 digitu.push_back(tmp[i]);
4105 } else {
4106 break;
4107 }
4108 }
4109
4110 if (!digitf.empty()) {
4111 int face;
4112 if (!parse_int(digitf, face)) {
4113 helios_runtime_error("ERROR (Context::loadOBJ): Face index on line " + std::to_string(lineno) + " must be a non-negative integer value.");
4114 }
4115 // Add bounds checking for face indices
4116 if (face <= 0 || face > vertices.size()) {
4117 helios_runtime_error("ERROR (Context::loadOBJ): Face vertex index " + std::to_string(face) + " on line " + std::to_string(lineno) + " is out of range. Valid range is 1-" + std::to_string(vertices.size()) +
4118 ". Check that vertex indices in face definitions reference existing vertices.");
4119 }
4120 f.push_back(face);
4121 }
4122 if (!digitu.empty()) {
4123 int uv;
4124 if (!parse_int(digitu, uv)) {
4125 helios_runtime_error("ERROR (Context::loadOBJ): u,v index on line " + std::to_string(lineno) + " must be a non-negative integer value.");
4126 }
4127 // Add bounds checking for UV indices
4128 if (uv <= 0 || uv > texture_uv.size()) {
4129 helios_runtime_error("ERROR (Context::loadOBJ): Texture coordinate index " + std::to_string(uv) + " on line " + std::to_string(lineno) + " is out of range. Valid range is 1-" + std::to_string(texture_uv.size()) +
4130 ". Check that texture coordinate indices in face definitions reference existing texture coordinates.");
4131 }
4132 u.push_back(uv);
4133 }
4134 }
4135 face_inds[current_material].push_back(f);
4136 texture_inds[current_material].push_back(u);
4137
4138 // ------ OTHER STUFF --------- //
4139 } else {
4140 getline(inputOBJ, line);
4141 }
4142 }
4143
4144 vec3 scl = scale;
4145 if (scl.x == 0 && scl.y == 0 && scl.z > 0) {
4146 if (boxmax - boxmin > 1e-6f) {
4147 scl = make_vec3(scale.z / (boxmax - boxmin), scale.z / (boxmax - boxmin), scale.z / (boxmax - boxmin));
4148 } else {
4149 // Object is flat or has zero height - use uniform scaling of requested height
4150 scl = make_vec3(scale.z, scale.z, scale.z);
4151 }
4152 } else {
4153 if (scl.x == 0 && (scl.y != 0 || scl.z != 0)) {
4154 std::cout << "WARNING (Context::loadOBJ): Scaling factor given for x-direction is zero. Setting scaling factor to 1" << std::endl;
4155 }
4156 if (scl.y == 0 && (scl.x != 0 || scl.z != 0)) {
4157 std::cout << "WARNING (Context::loadOBJ): Scaling factor given for y-direction is zero. Setting scaling factor to 1" << std::endl;
4158 }
4159 if (scl.z == 0 && (scl.x != 0 || scl.y != 0)) {
4160 std::cout << "WARNING (Context::loadOBJ): Scaling factor given for z-direction is zero. Setting scaling factor to 1" << std::endl;
4161 }
4162
4163 if (scl.x == 0) {
4164 scl.x = 1;
4165 }
4166 if (scl.y == 0) {
4167 scl.y = 1;
4168 }
4169 if (scl.z == 0) {
4170 scl.z = 1;
4171 }
4172 }
4173
4174 // Structure to hold triangle data for parallel processing
4175 struct TriangleData {
4176 vec3 vert0, vert1, vert2;
4177 std::string texture;
4178 vec2 uv0, uv1, uv2;
4179 RGBcolor color;
4180 bool hasTexture;
4181 bool textureColorIsOverridden;
4182 std::string materialname;
4183 std::string object;
4184 };
4185
4186 // Register all MTL materials in Context material system.
4187 // Material names from different OBJ files can collide (e.g., Blender's default "Material.001"),
4188 // so we generate unique names when a collision occurs and track the mapping.
4189 std::map<std::string, std::string> mtl_to_context_material;
4190 for (const auto &mat_entry : materials) {
4191 const std::string &matname = mat_entry.first;
4192 const OBJmaterial &mat = mat_entry.second;
4193 std::string context_matname = matname;
4194 if (doesMaterialExist(matname)) {
4195 int suffix = 1;
4196 do {
4197 context_matname = matname + "_" + std::to_string(suffix++);
4198 } while (doesMaterialExist(context_matname));
4199 }
4200 addMaterial(context_matname);
4201 setMaterialColor(context_matname, make_RGBAcolor(mat.color.r, mat.color.g, mat.color.b, 1));
4202 if (!mat.texture.empty()) {
4203 setMaterialTexture(context_matname, mat.texture);
4204 }
4205 setMaterialTextureColorOverride(context_matname, mat.textureColorIsOverridden);
4206 mtl_to_context_material[matname] = context_matname;
4207 }
4208
4209 std::vector<TriangleData> triangleDataList;
4210
4211 // First pass: Parallel data preparation - compute all triangle vertex data
4212 for (auto iter = face_inds.begin(); iter != face_inds.end(); ++iter) {
4213 std::string materialname = iter->first;
4214
4215 std::string texture;
4216 RGBcolor color = default_color;
4217 bool textureColorIsOverridden = false;
4218 bool textureHasTransparency = false;
4219
4220 if (materials.find(materialname) != materials.end()) {
4221 const OBJmaterial &mat = materials.at(materialname);
4222
4223 texture = mat.texture;
4224 color = mat.color;
4225 textureColorIsOverridden = mat.textureColorIsOverridden;
4226 textureHasTransparency = mat.textureHasTransparency;
4227 }
4228
4229 const auto &material_faces = face_inds.at(materialname);
4230 const auto &material_texture_inds = texture_inds.count(materialname) ? texture_inds.at(materialname) : std::vector<std::vector<int>>();
4231
4232 // Exception handling for OpenMP - capture exceptions and rethrow after parallel region
4233 std::string exception_message;
4234 bool exception_occurred = false;
4235
4236#ifdef USE_OPENMP
4237#pragma omp parallel for schedule(dynamic)
4238#endif
4239 for (int i = 0; i < static_cast<int>(material_faces.size()); i++) {
4240 try {
4241 for (uint t = 2; t < material_faces[i].size(); t++) {
4242 vec3 v0 = vertices.at(material_faces[i][0] - 1);
4243 vec3 v1 = vertices.at(material_faces[i][t - 1] - 1);
4244 vec3 v2 = vertices.at(material_faces[i][t] - 1);
4245
4246 if ((v0 - v1).magnitude() == 0 || (v0 - v2).magnitude() == 0 || (v1 - v2).magnitude() == 0) {
4247 continue;
4248 }
4249
4250 if (strcmp(upaxis, "YUP") == 0) {
4251 v0 = rotatePointAboutLine(v0, make_vec3(0, 0, 0), make_vec3(1, 0, 0), 0.5 * M_PI);
4252 v1 = rotatePointAboutLine(v1, make_vec3(0, 0, 0), make_vec3(1, 0, 0), 0.5 * M_PI);
4253 v2 = rotatePointAboutLine(v2, make_vec3(0, 0, 0), make_vec3(1, 0, 0), 0.5 * M_PI);
4254 }
4255
4256 v0 = rotatePoint(v0, rotation);
4257 v1 = rotatePoint(v1, rotation);
4258 v2 = rotatePoint(v2, rotation);
4259
4260 // Calculate final triangle vertices after transformations
4261 vec3 vert0 = origin + make_vec3(v0.x * scl.x, v0.y * scl.y, v0.z * scl.z);
4262 vec3 vert1 = origin + make_vec3(v1.x * scl.x, v1.y * scl.y, v1.z * scl.z);
4263 vec3 vert2 = origin + make_vec3(v2.x * scl.x, v2.y * scl.y, v2.z * scl.z);
4264
4265 // Check if triangle has sufficient area to avoid zero-area triangles
4266 float triangle_area = calculateTriangleArea(vert0, vert1, vert2);
4267
4268 if (triangle_area > MIN_TRIANGLE_AREA_THRESHOLD) { // Only process triangle if area is not negligible
4269 TriangleData triangleData;
4270 triangleData.vert0 = vert0;
4271 triangleData.vert1 = vert1;
4272 triangleData.vert2 = vert2;
4273 triangleData.texture = texture;
4274 triangleData.color = color;
4275 triangleData.textureColorIsOverridden = textureColorIsOverridden;
4276 triangleData.materialname = mtl_to_context_material.count(materialname) ? mtl_to_context_material.at(materialname) : materialname;
4277 triangleData.object = objects.at(material_faces[i][0] - 1);
4278
4279 // Handle texture coordinates if present
4280 // First check if material has texture file
4281 triangleData.hasTexture = !texture.empty();
4282
4283
4284 // If texture exists, try to get UV coordinates
4285 if (triangleData.hasTexture && i < material_texture_inds.size() && !material_texture_inds[i].empty() && t < material_texture_inds[i].size()) {
4286
4287 int iuv0 = material_texture_inds[i][0] - 1;
4288 int iuv1 = material_texture_inds[i][t - 1] - 1;
4289 int iuv2 = material_texture_inds[i][t] - 1;
4290
4291 if (iuv0 >= 0 && iuv0 < texture_uv.size() && iuv1 >= 0 && iuv1 < texture_uv.size() && iuv2 >= 0 && iuv2 < texture_uv.size()) {
4292 triangleData.uv0 = texture_uv.at(iuv0);
4293 triangleData.uv1 = texture_uv.at(iuv1);
4294 triangleData.uv2 = texture_uv.at(iuv2);
4295 } else {
4296 helios_runtime_error("ERROR (Context::loadOBJ): Invalid texture coordinate indices in face for material '" + materialname + "'. " + "UV indices [" + std::to_string(iuv0 + 1) + ", " + std::to_string(iuv1 + 1) + ", " +
4297 std::to_string(iuv2 + 1) + "] " + "exceed available UV coordinates (1-" + std::to_string(texture_uv.size()) + "). " +
4298 "Check that all face texture coordinate references in the OBJ file are valid.");
4299 }
4300 } else if (triangleData.hasTexture) {
4301 helios_runtime_error("ERROR (Context::loadOBJ): Material '" + materialname + "' specifies texture file '" + texture + "' " + "but face has no texture coordinates. Either remove the texture from the material " +
4302 "or add texture coordinates (vt) and face texture indices (f v1/vt1 v2/vt2 v3/vt3) to the OBJ file.");
4303 }
4304
4305#ifdef USE_OPENMP
4306#pragma omp critical
4307#endif
4308 {
4309 triangleDataList.push_back(triangleData);
4310 }
4311 }
4312 }
4313 } catch (const std::exception &e) {
4314 // Capture exception in OpenMP-safe way
4315#ifdef USE_OPENMP
4316#pragma omp critical
4317#endif
4318 {
4319 if (!exception_occurred) {
4320 exception_message = e.what();
4321 exception_occurred = true;
4322 }
4323 }
4324 }
4325 }
4326
4327 // Rethrow captured exception after parallel region
4328 if (exception_occurred) {
4329 helios_runtime_error(exception_message);
4330 }
4331 }
4332
4333 // Second pass: Sequential triangle creation to maintain thread safety
4334 for (const auto &triangleData: triangleDataList) {
4335 uint ID = 0;
4336
4337 if (triangleData.hasTexture) {
4338 ID = addTriangle(triangleData.vert0, triangleData.vert1, triangleData.vert2, triangleData.texture.c_str(), triangleData.uv0, triangleData.uv1, triangleData.uv2);
4339
4340 if (triangleData.textureColorIsOverridden) {
4341 setPrimitiveColor(ID, triangleData.color);
4343 }
4344 } else {
4345 ID = addTriangle(triangleData.vert0, triangleData.vert1, triangleData.vert2, triangleData.color);
4346 }
4347
4348 UUID.push_back(ID);
4349
4350 if (!triangleData.materialname.empty() && doesMaterialExist(triangleData.materialname)) {
4351 assignMaterialToPrimitive(ID, triangleData.materialname);
4352 }
4353
4354 if (triangleData.object != "none" && doesPrimitiveExist(ID)) {
4355 setPrimitiveData(ID, "object_label", triangleData.object);
4356 }
4357 }
4358
4359 if (!silent) {
4360 std::cout << "done." << std::endl;
4361 }
4362
4363 return UUID;
4364}
4365
4366std::map<std::string, Context::OBJmaterial> Context::loadMTL(const std::string &filebase, const std::string &material_file, const RGBcolor &default_color) {
4367 std::ifstream inputMTL;
4368
4369 std::string file = material_file;
4370
4371 // For relative paths, resolve relative to the OBJ file's directory (filebase)
4372 // For absolute paths, use unified file resolution
4373 std::filesystem::path resolved_path;
4374
4375 if (std::filesystem::path(file).is_absolute()) {
4376 // Absolute path - use unified resolution
4377 resolved_path = resolveFilePath(file);
4378 } else {
4379 // Relative path - resolve relative to OBJ file directory
4380 std::filesystem::path mtl_path = std::filesystem::path(filebase) / file;
4381 resolved_path = resolveFilePath(mtl_path.string());
4382 }
4383
4384 std::string resolved_file = resolved_path.string();
4385 inputMTL.open(resolved_file.c_str());
4386
4387 if (!inputMTL.is_open()) {
4388 helios_runtime_error("ERROR (Context::loadMTL): Could not open material file " + resolved_file + " after successful path resolution.");
4389 }
4390
4391 std::map<std::string, OBJmaterial> materials;
4392
4393 std::string line;
4394
4395 inputMTL >> line;
4396
4397 while (inputMTL.good()) {
4398 if (strcmp("#", line.c_str()) == 0) { // comments
4399 getline(inputMTL, line);
4400 inputMTL >> line;
4401 } else if (line == "newmtl") { // material library
4402 getline(inputMTL, line);
4403 std::string material_name = trim_whitespace(line);
4404 OBJmaterial mat(default_color, "", 0);
4405 materials.emplace(material_name, mat);
4406
4407 std::string map_Kd, map_d;
4408
4409 while (line != "newmtl" && inputMTL.good()) {
4410 inputMTL >> line;
4411
4412 if (line == "newmtl") {
4413 break;
4414 } else if (line == "map_a" || line == "map_Ka" || line == "Ks" || line == "Ka" || line == "map_Ks") {
4415 getline(inputMTL, line);
4416 } else if (line == "map_Kd" || line == "map_d") {
4417 std::string maptype = line;
4418 getline(inputMTL, line);
4419 line = trim_whitespace(line);
4420 std::istringstream stream(line);
4421 std::string tmp;
4422 while (stream.good()) {
4423 stream >> tmp;
4424 std::string ext = getFileExtension(tmp);
4425 if (ext == ".png" || ext == ".PNG" || ext == ".jpg" || ext == ".JPG" || ext == ".jpeg" || ext == ".JPEG") {
4426 std::string texturefile = tmp;
4427
4428 // Check for texture file existence using filesystem operations (more efficient)
4429 std::filesystem::path texture_path = texturefile;
4430 bool texture_exists = false;
4431
4432 // First try the path as given in MTL file
4433 if (std::filesystem::exists(texture_path)) {
4434 texture_exists = true;
4435 } else {
4436 // Try looking in the same directory where OBJ file is located
4437 texture_path = std::filesystem::path(filebase) / tmp;
4438 texturefile = texture_path.string();
4439 if (std::filesystem::exists(texture_path)) {
4440 texture_exists = true;
4441 }
4442 }
4443
4444 if (!texture_exists) {
4445 helios_runtime_error("ERROR (Context::loadOBJ): Texture file '" + tmp + "' referenced in .mtl file cannot be found. " + "Searched in current directory and OBJ file directory (" + filebase + "). " +
4446 "Ensure texture file exists or remove texture reference from material.");
4447 }
4448
4449 if (maptype == "map_d") {
4450 map_d = texturefile;
4451 } else {
4452 map_Kd = texturefile;
4453 }
4454 }
4455 }
4456 } else if (line == "Kd") {
4457 getline(inputMTL, line);
4458 std::string color_str = trim_whitespace(line);
4459 RGBAcolor color = string2RGBcolor(color_str.c_str());
4460 materials.at(material_name).color = make_RGBcolor(color.r, color.g, color.b);
4461 } else {
4462 getline(inputMTL, line);
4463 }
4464 }
4465
4466 if (!map_Kd.empty()) {
4467 materials.at(material_name).texture = map_Kd;
4468 if (!map_d.empty() && map_d != map_Kd) {
4469 materials.at(material_name).textureHasTransparency = true;
4470 }
4471 } else if (!map_d.empty()) {
4472 materials.at(material_name).texture = map_d;
4473 materials.at(material_name).textureColorIsOverridden = true;
4474 }
4475 } else {
4476 getline(inputMTL, line);
4477 inputMTL >> line;
4478 }
4479 }
4480
4481 return materials;
4482}
4483
4484void Context::writeOBJ(const std::string &filename, bool write_normals, bool silent) const {
4485 writeOBJ(filename, getAllUUIDs(), {}, write_normals, silent);
4486}
4487
4488void Context::writeOBJ(const std::string &filename, const std::vector<uint> &UUIDs, bool write_normals, bool silent) const {
4489 writeOBJ(filename, UUIDs, {}, write_normals, silent);
4490}
4491
4492void Context::writeOBJ(const std::string &filename, const std::vector<uint> &UUIDs, const std::vector<std::string> &primitive_dat_fields, bool write_normals, bool silent) const {
4493
4494 if (UUIDs.empty()) {
4495 std::cout << "WARNING (Context::writeOBJ): No primitives found to write - OBJ file " << filename << " will not be written." << std::endl;
4496 return;
4497 }
4498 if (filename.empty()) {
4499 std::cout << "WARNING (Context::writeOBJ): Filename was empty - OBJ file " << filename << " will not be written." << std::endl;
4500 return;
4501 }
4502
4503 std::string objfilename = filename;
4504 std::string mtlfilename = filename;
4505
4506 auto file_extension = getFileExtension(filename);
4507 auto file_stem = getFileStem(filename);
4508 auto file_path = getFilePath(filename);
4509
4510 if (file_extension != ".obj" && file_extension != ".OBJ") { // append obj to file name
4511 objfilename.append(".obj");
4512 mtlfilename.append(".mtl");
4513 } else {
4514 if (!file_path.empty()) {
4515 std::filesystem::path mtl_path = std::filesystem::path(file_path) / (file_stem + ".mtl");
4516 mtlfilename = mtl_path.string();
4517 } else {
4518 mtlfilename = file_stem + ".mtl";
4519 }
4520 }
4521
4522 if (!file_path.empty() && !std::filesystem::exists(file_path)) {
4523 if (!std::filesystem::create_directory(file_path)) {
4524 std::cerr << "failed. Directory " << file_path << " does not exist and it could not be created - OBJ file will not be written." << std::endl;
4525 return;
4526 }
4527 }
4528
4529 if (!silent) {
4530 std::cout << "Writing OBJ file " << objfilename << "..." << std::flush;
4531 }
4532
4533 std::vector<OBJmaterial> materials;
4534 std::unordered_map<std::string, uint> material_cache;
4535 const size_t primitive_count = UUIDs.size();
4536 const size_t estimated_vertices = primitive_count * 4;
4537
4538 std::vector<vec3> verts;
4539 verts.reserve(estimated_vertices);
4540 std::vector<vec3> normals;
4541 if (write_normals) {
4542 normals.reserve(primitive_count);
4543 }
4544 std::vector<vec2> uv;
4545 uv.reserve(estimated_vertices);
4546
4547 std::map<uint, std::vector<int3>> faces;
4548 std::map<uint, std::vector<int>> normal_inds;
4549 std::map<uint, std::vector<int3>> uv_inds;
4550 size_t vertex_count = 1;
4551 size_t normal_count = 0;
4552 size_t uv_count = 1;
4553 std::map<uint, std::vector<uint>> UUIDs_write;
4554
4555 std::map<std::string, std::map<uint, std::vector<int3>>> object_faces;
4556 std::map<std::string, std::map<uint, std::vector<int>>> object_normal_inds;
4557 std::map<std::string, std::map<uint, std::vector<int3>>> object_uv_inds;
4558 std::vector<std::string> object_order;
4559 object_order.reserve(primitive_count / 10);
4560 bool object_groups_found = false;
4561
4562 for (size_t p: UUIDs) {
4563 if (!doesPrimitiveExist(p)) {
4564 std::ostringstream err_stream;
4565 err_stream << "ERROR (Context::writeOBJ): Primitive with UUID " << p << " does not exist. "
4566 << "Ensure all UUIDs in the input vector correspond to valid primitives before calling writeOBJ.";
4567 helios_runtime_error(err_stream.str());
4568 }
4569
4570 const Primitive *prim_ptr = getPrimitivePointer_private(p);
4571
4572 if (prim_ptr->getType() == PRIMITIVE_TYPE_VOXEL) {
4573 std::ostringstream err_stream;
4574 err_stream << "ERROR (Context::writeOBJ): Voxel primitives (UUID " << p << ") cannot be written to OBJ format. "
4575 << "OBJ format only supports surface primitives (triangles, patches). "
4576 << "Filter out voxel primitives before calling writeOBJ.";
4577 helios_runtime_error(err_stream.str());
4578 }
4579
4580 std::vector<vec3> vertices = prim_ptr->getVertices();
4581 PrimitiveType type = prim_ptr->getType();
4582 RGBcolor C = prim_ptr->getColor();
4583 std::string texturefile = prim_ptr->getTextureFile();
4584 bool texture_color_overridden = prim_ptr->isTextureColorOverridden();
4585
4586 std::string obj_label = "default";
4587 if (doesPrimitiveDataExist(p, "object_label")) {
4588 getPrimitiveData(p, "object_label", obj_label);
4589 object_groups_found = true;
4590 }
4591 if (object_faces.find(obj_label) == object_faces.end()) {
4592 object_faces[obj_label] = {};
4593 object_normal_inds[obj_label] = {};
4594 object_uv_inds[obj_label] = {};
4595 object_order.push_back(obj_label);
4596 }
4597
4598 std::string material_key = texturefile + "|" + std::to_string(C.r) + "," + std::to_string(C.g) + "," + std::to_string(C.b) + "|" + std::to_string(texture_color_overridden);
4599
4600 uint material_ID;
4601 auto material_iter = material_cache.find(material_key);
4602
4603 if (material_iter != material_cache.end()) {
4604 // Material exists in cache
4605 material_ID = material_iter->second;
4606 } else {
4607 // Create new material
4608 OBJmaterial mat(C, texturefile, materials.size());
4609 materials.emplace_back(mat);
4610 material_ID = mat.materialID;
4611
4613 materials.back().textureHasTransparency = true;
4614 }
4615 if (texture_color_overridden) {
4616 materials.back().textureColorIsOverridden = true;
4617 }
4618
4619 material_cache[material_key] = material_ID;
4620 }
4621
4622 if (!primitive_dat_fields.empty()) {
4623 UUIDs_write[material_ID].push_back(p);
4624 }
4625
4626 if (write_normals) {
4627 vec3 normal = getPrimitiveNormal(p);
4628 normals.push_back(normal);
4629 normal_count++;
4630 }
4631
4632 if (type == PRIMITIVE_TYPE_TRIANGLE) {
4633 int3 ftmp = make_int3((int) vertex_count, (int) vertex_count + 1, (int) vertex_count + 2);
4634 faces[material_ID].push_back(ftmp);
4635 object_faces[obj_label][material_ID].push_back(ftmp);
4636 for (int i = 0; i < 3; i++) {
4637 verts.push_back(vertices.at(i));
4638 vertex_count++;
4639 }
4640
4641 if (write_normals) {
4642 normal_inds[material_ID].push_back(static_cast<int>(normal_count));
4643 object_normal_inds[obj_label][material_ID].push_back(static_cast<int>(normal_count));
4644 }
4645
4646 std::vector<vec2> uv_v = getTrianglePointer_private(p)->getTextureUV();
4647 if (getTrianglePointer_private(p)->hasTexture()) {
4648 int3 tuv = make_int3((int) uv_count, (int) uv_count + 1, (int) uv_count + 2);
4649 uv_inds[material_ID].push_back(tuv);
4650 object_uv_inds[obj_label][material_ID].push_back(tuv);
4651 for (int i = 0; i < 3; i++) {
4652 uv.push_back(uv_v.at(i));
4653 uv_count++;
4654 }
4655 } else {
4656 int3 tuv = make_int3(-1, -1, -1);
4657 uv_inds[material_ID].push_back(tuv);
4658 object_uv_inds[obj_label][material_ID].push_back(tuv);
4659 }
4660 } else if (type == PRIMITIVE_TYPE_PATCH) {
4661 int3 ftmp1 = make_int3((int) vertex_count, (int) vertex_count + 1, (int) vertex_count + 2);
4662 int3 ftmp2 = make_int3((int) vertex_count, (int) vertex_count + 2, (int) vertex_count + 3);
4663 faces[material_ID].push_back(ftmp1);
4664 faces[material_ID].push_back(ftmp2);
4665 object_faces[obj_label][material_ID].push_back(ftmp1);
4666 object_faces[obj_label][material_ID].push_back(ftmp2);
4667 for (int i = 0; i < 4; i++) {
4668 verts.push_back(vertices.at(i));
4669 vertex_count++;
4670 }
4671 std::vector<vec2> uv_v;
4672 uv_v = getPatchPointer_private(p)->getTextureUV();
4673
4674 if (write_normals) {
4675 normal_inds[material_ID].push_back(static_cast<int>(normal_count));
4676 normal_inds[material_ID].push_back(static_cast<int>(normal_count));
4677 object_normal_inds[obj_label][material_ID].push_back(static_cast<int>(normal_count));
4678 object_normal_inds[obj_label][material_ID].push_back(static_cast<int>(normal_count));
4679 }
4680
4681 if (getPatchPointer_private(p)->hasTexture()) {
4682 int3 tuv1 = make_int3((int) uv_count, (int) uv_count + 1, (int) uv_count + 2);
4683 int3 tuv2 = make_int3((int) uv_count, (int) uv_count + 2, (int) uv_count + 3);
4684 uv_inds[material_ID].push_back(tuv1);
4685 uv_inds[material_ID].push_back(tuv2);
4686 object_uv_inds[obj_label][material_ID].push_back(tuv1);
4687 object_uv_inds[obj_label][material_ID].push_back(tuv2);
4688 if (uv_v.empty()) { // default (u,v)
4689 uv.push_back(make_vec2(0, 1));
4690 uv.push_back(make_vec2(1, 1));
4691 uv.push_back(make_vec2(1, 0));
4692 uv.push_back(make_vec2(0, 0));
4693 uv_count += 4;
4694 } else { // custom (u,v)
4695 for (int i = 0; i < 4; i++) {
4696 uv.push_back(uv_v.at(i));
4697 uv_count++;
4698 }
4699 }
4700 } else {
4701 int3 tuv = make_int3(-1, -1, -1);
4702 uv_inds[material_ID].push_back(tuv);
4703 uv_inds[material_ID].push_back(tuv);
4704 object_uv_inds[obj_label][material_ID].push_back(tuv);
4705 object_uv_inds[obj_label][material_ID].push_back(tuv);
4706 }
4707 }
4708 }
4709
4710 if (write_normals)
4711 assert(normal_inds.size() == faces.size());
4712 // assert(verts.size() == faces.size());
4713 assert(uv_inds.size() == faces.size());
4714 for (int i = 0; i < faces.size(); i++) {
4715 assert(uv_inds.at(i).size() == faces.at(i).size());
4716 }
4717
4718 // copy material textures to new directory and edit old file paths
4719 std::filesystem::path output_path = std::filesystem::path(file_path);
4720 std::filesystem::path texture_dir = output_path.parent_path();
4721
4722 // If no parent path (filename only), use current directory
4723 if (texture_dir.empty()) {
4724 texture_dir = ".";
4725 }
4726
4727 for (auto &material: materials) {
4728 std::string texture = material.texture;
4729 if (!texture.empty()) {
4730 std::error_code ec;
4731 std::filesystem::path source_path = std::filesystem::absolute(texture, ec);
4732
4733 // If we can't resolve the absolute path, try the original path
4734 if (ec) {
4735 source_path = std::filesystem::path(texture);
4736 }
4737
4738 if (!std::filesystem::exists(source_path)) {
4739 // Skip missing texture files silently (maintain original behavior)
4740 continue;
4741 }
4742
4743 auto filename = source_path.filename();
4744 std::filesystem::path dest_path = texture_dir / filename;
4745
4746 // Skip copying if source and destination are the same file
4747 bool same_file = false;
4748 try {
4749 same_file = std::filesystem::equivalent(source_path, dest_path, ec);
4750 if (ec)
4751 same_file = false; // If we can't determine equivalence, assume different
4752 } catch (...) {
4753 same_file = false;
4754 }
4755
4756 if (same_file) {
4757 material.texture = filename.string();
4758 continue;
4759 }
4760
4761 // Attempt to copy file, but don't fail if it doesn't work
4762 try {
4763 std::filesystem::copy_file(source_path, dest_path, std::filesystem::copy_options::overwrite_existing, ec);
4764 if (!ec) {
4765 material.texture = filename.string();
4766 } // else keep original texture path
4767 } catch (...) {
4768 // If copy fails for any reason, keep original texture path
4769 // This maintains backward compatibility
4770 }
4771 }
4772 }
4773
4774 std::ofstream objfstream;
4775 objfstream.open(objfilename);
4776 std::ofstream mtlfstream;
4777 mtlfstream.open(mtlfilename);
4778
4779 objfstream << "# Helios auto-generated OBJ File" << std::endl;
4780 objfstream << "# baileylab.ucdavis.edu/software/helios" << std::endl;
4781 objfstream << "mtllib " << getFileName(mtlfilename) << std::endl;
4782
4783 // Parallel string formatting for vertices, normals, and UV coordinates
4784 std::vector<std::string> vertex_chunks;
4785 const int num_threads = std::min(static_cast<int>(verts.size() / 1000 + 1), std::max(1, static_cast<int>(std::thread::hardware_concurrency())));
4786 vertex_chunks.resize(num_threads);
4787
4788#ifdef USE_OPENMP
4789#pragma omp parallel num_threads(num_threads)
4790#endif
4791 {
4792 int tid = 0;
4793#ifdef USE_OPENMP
4794 tid = omp_get_thread_num();
4795#endif
4796 std::ostringstream vertex_stream;
4797 vertex_stream.precision(8);
4798
4799 const size_t chunk_size = (verts.size() + num_threads - 1) / num_threads;
4800 const size_t start_idx = tid * chunk_size;
4801 const size_t end_idx = std::min(start_idx + chunk_size, verts.size());
4802
4803 for (size_t i = start_idx; i < end_idx; i++) {
4804 vertex_stream << "v " << verts[i].x << " " << verts[i].y << " " << verts[i].z << "\n";
4805 }
4806
4807 vertex_chunks[tid] = vertex_stream.str();
4808 }
4809
4810 for (const auto &chunk: vertex_chunks) {
4811 objfstream << chunk;
4812 }
4813
4814 if (write_normals) {
4815 std::vector<std::string> normal_chunks;
4816 normal_chunks.resize(num_threads);
4817
4818#ifdef USE_OPENMP
4819#pragma omp parallel num_threads(num_threads)
4820#endif
4821 {
4822 int tid = 0;
4823#ifdef USE_OPENMP
4824 tid = omp_get_thread_num();
4825#endif
4826 std::ostringstream normal_stream;
4827 normal_stream.precision(8);
4828
4829 const size_t chunk_size = (normals.size() + num_threads - 1) / num_threads;
4830 const size_t start_idx = tid * chunk_size;
4831 const size_t end_idx = std::min(start_idx + chunk_size, normals.size());
4832
4833 const float epsilon = 1e-7;
4834 for (size_t i = start_idx; i < end_idx; i++) {
4835 vec3 n = normals[i];
4836 if (std::abs(n.x) < epsilon)
4837 n.x = 0;
4838 if (std::abs(n.y) < epsilon)
4839 n.y = 0;
4840 if (std::abs(n.z) < epsilon)
4841 n.z = 0;
4842 normal_stream << "vn " << n.x << " " << n.y << " " << n.z << "\n";
4843 }
4844
4845 normal_chunks[tid] = normal_stream.str();
4846 }
4847
4848 for (const auto &chunk: normal_chunks) {
4849 objfstream << chunk;
4850 }
4851 }
4852
4853 if (!uv.empty()) {
4854 std::vector<std::string> uv_chunks;
4855 uv_chunks.resize(num_threads);
4856
4857#ifdef USE_OPENMP
4858#pragma omp parallel num_threads(num_threads)
4859#endif
4860 {
4861 int tid = 0;
4862#ifdef USE_OPENMP
4863 tid = omp_get_thread_num();
4864#endif
4865 std::ostringstream uv_stream;
4866 uv_stream.precision(8);
4867
4868 const size_t chunk_size = (uv.size() + num_threads - 1) / num_threads;
4869 const size_t start_idx = tid * chunk_size;
4870 const size_t end_idx = std::min(start_idx + chunk_size, uv.size());
4871
4872 for (size_t i = start_idx; i < end_idx; i++) {
4873 uv_stream << "vt " << uv[i].x << " " << uv[i].y << "\n";
4874 }
4875
4876 uv_chunks[tid] = uv_stream.str();
4877 }
4878
4879 for (const auto &chunk: uv_chunks) {
4880 objfstream << chunk;
4881 }
4882 }
4883
4884 // Parallel face string generation
4885
4886 if (object_groups_found) {
4887 // Process object groups sequentially (maintain OBJ structure)
4888 // but parallelize face generation within each material group
4889 for (const auto &obj_label: object_order) {
4890 objfstream << "o " << obj_label << "\n";
4891
4892 for (int mat = 0; mat < materials.size(); mat++) {
4893 auto fit = object_faces[obj_label].find(mat);
4894 if (fit == object_faces[obj_label].end())
4895 continue;
4896
4897 objfstream << "usemtl material" << mat << "\n";
4898
4899 const auto &current_faces = fit->second;
4900 if (current_faces.size() > 100) { // Only parallelize if enough faces
4901 // Parallel face string generation for this material
4902 std::vector<std::string> face_chunks;
4903 face_chunks.resize(num_threads);
4904
4905#ifdef USE_OPENMP
4906#pragma omp parallel num_threads(num_threads)
4907#endif
4908 {
4909 int tid = 0;
4910#ifdef USE_OPENMP
4911 tid = omp_get_thread_num();
4912#endif
4913 std::ostringstream face_stream;
4914
4915 const size_t chunk_size = (current_faces.size() + num_threads - 1) / num_threads;
4916 const size_t start_idx = tid * chunk_size;
4917 const size_t end_idx = std::min(start_idx + chunk_size, current_faces.size());
4918
4919 for (size_t f = start_idx; f < end_idx; f++) {
4920 if (uv.empty()) {
4921 if (write_normals) {
4922 face_stream << "f " << current_faces[f].x << "//" << object_normal_inds[obj_label][mat][f] << " " << current_faces[f].y << "//" << object_normal_inds[obj_label][mat][f] << " " << current_faces[f].z << "//"
4923 << object_normal_inds[obj_label][mat][f] << "\n";
4924 } else {
4925 face_stream << "f " << current_faces[f].x << " " << current_faces[f].y << " " << current_faces[f].z << "\n";
4926 }
4927 } else if (object_uv_inds[obj_label][mat][f].x < 0) {
4928 face_stream << "f " << current_faces[f].x << "/1 " << current_faces[f].y << "/1 " << current_faces[f].z << "/1\n";
4929 } else {
4930 if (write_normals) {
4931 face_stream << "f " << current_faces[f].x << "/" << object_uv_inds[obj_label][mat][f].x << "/" << object_normal_inds[obj_label][mat][f] << " " << current_faces[f].y << "/" << object_uv_inds[obj_label][mat][f].y
4932 << "/" << object_normal_inds[obj_label][mat][f] << " " << current_faces[f].z << "/" << object_uv_inds[obj_label][mat][f].z << "/" << object_normal_inds[obj_label][mat][f] << "\n";
4933 } else {
4934 face_stream << "f " << current_faces[f].x << "/" << object_uv_inds[obj_label][mat][f].x << " " << current_faces[f].y << "/" << object_uv_inds[obj_label][mat][f].y << " " << current_faces[f].z << "/"
4935 << object_uv_inds[obj_label][mat][f].z << "\n";
4936 }
4937 }
4938 }
4939
4940 face_chunks[tid] = face_stream.str();
4941 }
4942
4943 // Sequential write of face chunks
4944 for (const auto &chunk: face_chunks) {
4945 objfstream << chunk;
4946 }
4947 } else {
4948 // For small face counts, use original sequential approach
4949 for (size_t f = 0; f < current_faces.size(); ++f) {
4950 if (uv.empty()) {
4951 if (write_normals) {
4952 objfstream << "f " << current_faces[f].x << "//" << object_normal_inds[obj_label][mat][f] << " " << current_faces[f].y << "//" << object_normal_inds[obj_label][mat][f] << " " << current_faces[f].z << "//"
4953 << object_normal_inds[obj_label][mat][f] << std::endl;
4954 } else {
4955 objfstream << "f " << current_faces[f].x << " " << current_faces[f].y << " " << current_faces[f].z << std::endl;
4956 }
4957 } else if (object_uv_inds[obj_label][mat][f].x < 0) {
4958 objfstream << "f " << current_faces[f].x << "/1 " << current_faces[f].y << "/1 " << current_faces[f].z << "/1" << std::endl;
4959 } else {
4960 if (write_normals) {
4961 objfstream << "f " << current_faces[f].x << "/" << object_uv_inds[obj_label][mat][f].x << "/" << object_normal_inds[obj_label][mat][f] << " " << current_faces[f].y << "/" << object_uv_inds[obj_label][mat][f].y << "/"
4962 << object_normal_inds[obj_label][mat][f] << " " << current_faces[f].z << "/" << object_uv_inds[obj_label][mat][f].z << "/" << object_normal_inds[obj_label][mat][f] << std::endl;
4963 } else {
4964 objfstream << "f " << current_faces[f].x << "/" << object_uv_inds[obj_label][mat][f].x << " " << current_faces[f].y << "/" << object_uv_inds[obj_label][mat][f].y << " " << current_faces[f].z << "/"
4965 << object_uv_inds[obj_label][mat][f].z << std::endl;
4966 }
4967 }
4968 }
4969 }
4970 }
4971 }
4972 } else {
4973 // No object groups - simpler structure, better parallelization opportunity
4974 for (int mat = 0; mat < materials.size(); mat++) {
4975 assert(materials.at(mat).materialID == mat);
4976 objfstream << "usemtl material" << mat << "\n";
4977
4978 const auto &current_faces = faces.at(mat);
4979 if (current_faces.size() > 100) { // Only parallelize if enough faces
4980 // Parallel face string generation for this material
4981 std::vector<std::string> face_chunks;
4982 face_chunks.resize(num_threads);
4983
4984#ifdef USE_OPENMP
4985#pragma omp parallel num_threads(num_threads)
4986#endif
4987 {
4988 int tid = 0;
4989#ifdef USE_OPENMP
4990 tid = omp_get_thread_num();
4991#endif
4992 std::ostringstream face_stream;
4993
4994 const size_t chunk_size = (current_faces.size() + num_threads - 1) / num_threads;
4995 const size_t start_idx = tid * chunk_size;
4996 const size_t end_idx = std::min(start_idx + chunk_size, current_faces.size());
4997
4998 for (size_t f = start_idx; f < end_idx; f++) {
4999 if (uv.empty()) {
5000 if (write_normals) {
5001 face_stream << "f " << current_faces[f].x << "//" << normal_inds.at(mat)[f] << " " << current_faces[f].y << "//" << normal_inds.at(mat)[f] << " " << current_faces[f].z << "//" << normal_inds.at(mat)[f] << "\n";
5002 } else {
5003 face_stream << "f " << current_faces[f].x << " " << current_faces[f].y << " " << current_faces[f].z << "\n";
5004 }
5005 } else if (uv_inds.at(mat)[f].x < 0) {
5006 face_stream << "f " << current_faces[f].x << "/1 " << current_faces[f].y << "/1 " << current_faces[f].z << "/1\n";
5007 } else {
5008 if (write_normals) {
5009 face_stream << "f " << current_faces[f].x << "/" << uv_inds.at(mat)[f].x << "/" << normal_inds.at(mat)[f] << " " << current_faces[f].y << "/" << uv_inds.at(mat)[f].y << "/" << normal_inds.at(mat)[f] << " "
5010 << current_faces[f].z << "/" << uv_inds.at(mat)[f].z << "/" << normal_inds.at(mat)[f] << "\n";
5011 } else {
5012 face_stream << "f " << current_faces[f].x << "/" << uv_inds.at(mat)[f].x << " " << current_faces[f].y << "/" << uv_inds.at(mat)[f].y << " " << current_faces[f].z << "/" << uv_inds.at(mat)[f].z << "\n";
5013 }
5014 }
5015 }
5016
5017 face_chunks[tid] = face_stream.str();
5018 }
5019
5020 // Sequential write of face chunks
5021 for (const auto &chunk: face_chunks) {
5022 objfstream << chunk;
5023 }
5024 } else {
5025 // For small face counts, use original sequential approach
5026 for (int f = 0; f < current_faces.size(); f++) {
5027 if (uv.empty()) {
5028 if (write_normals) {
5029 objfstream << "f " << current_faces[f].x << "//" << normal_inds.at(mat)[f] << " " << current_faces[f].y << "//" << normal_inds.at(mat)[f] << " " << current_faces[f].z << "//" << normal_inds.at(mat)[f] << std::endl;
5030 } else {
5031 objfstream << "f " << current_faces[f].x << " " << current_faces[f].y << " " << current_faces[f].z << std::endl;
5032 }
5033 } else if (uv_inds.at(mat)[f].x < 0) {
5034 objfstream << "f " << current_faces[f].x << "/1 " << current_faces[f].y << "/1 " << current_faces[f].z << "/1" << std::endl;
5035 } else {
5036 if (write_normals) {
5037 objfstream << "f " << current_faces[f].x << "/" << uv_inds.at(mat)[f].x << "/" << normal_inds.at(mat)[f] << " " << current_faces[f].y << "/" << uv_inds.at(mat)[f].y << "/" << normal_inds.at(mat)[f] << " "
5038 << current_faces[f].z << "/" << uv_inds.at(mat)[f].z << "/" << normal_inds.at(mat)[f] << std::endl;
5039 } else {
5040 objfstream << "f " << current_faces[f].x << "/" << uv_inds.at(mat)[f].x << " " << current_faces[f].y << "/" << uv_inds.at(mat)[f].y << " " << current_faces[f].z << "/" << uv_inds.at(mat)[f].z << std::endl;
5041 }
5042 }
5043 }
5044 }
5045 }
5046 }
5047
5048 for (int mat = 0; mat < materials.size(); mat++) {
5049 if (materials.at(mat).texture.empty()) {
5050 RGBcolor current_color = materials.at(mat).color;
5051 mtlfstream << "newmtl material" << mat << std::endl;
5052 mtlfstream << "Ka " << current_color.r << " " << current_color.g << " " << current_color.b << std::endl;
5053 mtlfstream << "Kd " << current_color.r << " " << current_color.g << " " << current_color.b << std::endl;
5054 mtlfstream << "Ks 0.0 0.0 0.0" << std::endl;
5055 mtlfstream << "illum 2 " << std::endl;
5056 } else {
5057 std::string current_texture = materials.at(mat).texture;
5058 mtlfstream << "newmtl material" << mat << std::endl;
5059 if (materials.at(mat).textureColorIsOverridden) {
5060 RGBcolor current_color = materials.at(mat).color;
5061 mtlfstream << "Ka " << current_color.r << " " << current_color.g << " " << current_color.b << std::endl;
5062 mtlfstream << "Kd " << current_color.r << " " << current_color.g << " " << current_color.b << std::endl;
5063 } else {
5064 mtlfstream << "map_Kd " << current_texture << std::endl;
5065 }
5066 if (materials.at(mat).textureHasTransparency) {
5067 mtlfstream << "map_d " << current_texture << std::endl;
5068 }
5069 mtlfstream << "Ks 0.0 0.0 0.0" << std::endl;
5070 mtlfstream << "illum 2 " << std::endl;
5071 }
5072 }
5073
5074 objfstream.close();
5075 mtlfstream.close();
5076
5077 if (!primitive_dat_fields.empty()) {
5078 bool uuidexistswarning = false;
5079 bool dataexistswarning = false;
5080 bool datatypewarning = false;
5081
5082 for (const std::string &label: primitive_dat_fields) {
5083 std::filesystem::path dat_path = std::filesystem::path(file_path) / (file_stem + "_" + std::string(label) + ".dat");
5084 std::string datfilename = dat_path.string();
5085 std::ofstream datout(datfilename);
5086
5087 for (int mat = 0; mat < materials.size(); mat++) {
5088 for (uint UUID: UUIDs_write.at(mat)) {
5089 if (!doesPrimitiveExist(UUID)) {
5090 uuidexistswarning = true;
5091 continue;
5092 }
5093
5094 // a patch is converted to 2 triangles, so need to write 2 data values for patches
5095 int Nprims = 1;
5097 Nprims = 2;
5098 }
5099
5100 if (!doesPrimitiveDataExist(UUID, label.c_str())) {
5101 dataexistswarning = true;
5102 for (int i = 0; i < Nprims; i++) {
5103 datout << 0 << std::endl;
5104 }
5105 continue;
5106 }
5107
5108 HeliosDataType type = getPrimitiveDataType(label.c_str());
5109 if (type == HELIOS_TYPE_INT) {
5110 int data;
5111 getPrimitiveData(UUID, label.c_str(), data);
5112 for (int i = 0; i < Nprims; i++) {
5113 datout << data << std::endl;
5114 }
5115 } else if (type == HELIOS_TYPE_UINT) {
5116 uint data;
5117 getPrimitiveData(UUID, label.c_str(), data);
5118 for (int i = 0; i < Nprims; i++) {
5119 datout << data << std::endl;
5120 }
5121 } else if (type == HELIOS_TYPE_FLOAT) {
5122 float data;
5123 getPrimitiveData(UUID, label.c_str(), data);
5124 for (int i = 0; i < Nprims; i++) {
5125 datout << data << std::endl;
5126 }
5127 } else if (type == HELIOS_TYPE_DOUBLE) {
5128 double data;
5129 getPrimitiveData(UUID, label.c_str(), data);
5130 for (int i = 0; i < Nprims; i++) {
5131 datout << data << std::endl;
5132 }
5133 } else if (type == HELIOS_TYPE_STRING) {
5134 std::string data;
5135 getPrimitiveData(UUID, label.c_str(), data);
5136 for (int i = 0; i < Nprims; i++) {
5137 datout << data << std::endl;
5138 }
5139 } else {
5140 datatypewarning = true;
5141 for (int i = 0; i < Nprims; i++) {
5142 datout << 0 << std::endl;
5143 }
5144 }
5145 }
5146 }
5147
5148 datout.close();
5149 }
5150
5151 if (uuidexistswarning) {
5152 helios_runtime_error("Context::writeOBJ: One or more UUIDs do not exist in the Context. Cannot write OBJ file with invalid primitives.");
5153 }
5154 if (dataexistswarning) {
5155 helios_runtime_error("Context::writeOBJ: Primitive data requested did not exist for one or more primitives. Cannot write incomplete data to OBJ file.");
5156 }
5157 if (datatypewarning) {
5158 helios_runtime_error("Context::writeOBJ: Only scalar primitive data types (uint, int, float, double, and string) are supported for primitive data export.");
5159 }
5160 }
5161}
5162
5163void Context::writePrimitiveData(const std::string &filename, const std::vector<std::string> &column_format, bool print_header) const {
5164 writePrimitiveData(filename, column_format, getAllUUIDs(), print_header);
5165}
5166
5167void Context::writePrimitiveData(const std::string &filename, const std::vector<std::string> &column_format, const std::vector<uint> &UUIDs, bool print_header) const {
5168 std::ofstream file(filename);
5169
5170 if (print_header) {
5171 for (const auto &label: column_format) {
5172 file << label << " ";
5173 }
5174 file.seekp(-1, std::ios_base::end);
5175 file << "\n";
5176 }
5177
5178 bool uuidexistswarning = false;
5179 bool dataexistswarning = false;
5180 bool datatypewarning = false;
5181
5182 for (uint UUID: UUIDs) {
5183 if (!doesPrimitiveExist(UUID)) {
5184 uuidexistswarning = true;
5185 continue;
5186 }
5187 for (const auto &label: column_format) {
5188 if (label == "UUID") {
5189 file << UUID << " ";
5190 continue;
5191 }
5192 if (!doesPrimitiveDataExist(UUID, label.c_str())) {
5193 dataexistswarning = true;
5194 file << 0 << " ";
5195 continue;
5196 }
5197 HeliosDataType type = getPrimitiveDataType(label.c_str());
5198 if (type == HELIOS_TYPE_INT) {
5199 int data;
5200 getPrimitiveData(UUID, label.c_str(), data);
5201 file << data << " ";
5202 } else if (type == HELIOS_TYPE_UINT) {
5203 uint data;
5204 getPrimitiveData(UUID, label.c_str(), data);
5205 file << data << " ";
5206 } else if (type == HELIOS_TYPE_FLOAT) {
5207 float data;
5208 getPrimitiveData(UUID, label.c_str(), data);
5209 file << data << " ";
5210 } else if (type == HELIOS_TYPE_DOUBLE) {
5211 double data;
5212 getPrimitiveData(UUID, label.c_str(), data);
5213 file << data << " ";
5214 } else if (type == HELIOS_TYPE_STRING) {
5215 std::string data;
5216 getPrimitiveData(UUID, label.c_str(), data);
5217 file << data << " ";
5218 } else {
5219 datatypewarning = true;
5220 file << 0 << " ";
5221 }
5222 }
5223 file.seekp(-1, std::ios_base::end);
5224 file << "\n";
5225 }
5226
5227 if (uuidexistswarning) {
5228 std::cerr << "WARNING (Context::writePrimitiveData): Vector of UUIDs passed to writePrimitiveData() function contained UUIDs that do not exist, which were skipped." << std::endl;
5229 }
5230 if (dataexistswarning) {
5231 std::cerr << "WARNING (Context::writePrimitiveData): Primitive data requested did not exist for one or more primitives. A default value of 0 was written in these cases." << std::endl;
5232 }
5233 if (datatypewarning) {
5234 std::cerr << "WARNING (Context::writePrimitiveData): Only scalar primitive data types (uint, int, float, and double) are supported for this function. A column of 0's was written in these cases." << std::endl;
5235 }
5236
5237 file.close();
5238}
5239
5240namespace {
5241
5242 // Parse a date string with '-' or '/' delimiters, or compact 8-digit YYYYMMDD format.
5243 Date parseDateString(const std::string &datestr, const std::string &date_string_format, size_t row, const std::string &data_file) {
5244
5245 // Check for compact 8-digit format (no delimiters)
5246 if (datestr.find('-') == std::string::npos && datestr.find('/') == std::string::npos) {
5247 if (datestr.size() == 8) {
5248 // Compact 8-digit date: parse according to format
5249 int year, month, day;
5250 if (date_string_format == "YYYYMMDD" || date_string_format == "YYYY-MM-DD") {
5251 year = std::stoi(datestr.substr(0, 4));
5252 month = std::stoi(datestr.substr(4, 2));
5253 day = std::stoi(datestr.substr(6, 2));
5254 } else if (date_string_format == "DDMMYYYY" || date_string_format == "DD-MM-YYYY" || date_string_format == "DD/MM/YYYY") {
5255 day = std::stoi(datestr.substr(0, 2));
5256 month = std::stoi(datestr.substr(2, 2));
5257 year = std::stoi(datestr.substr(4, 4));
5258 } else if (date_string_format == "MMDDYYYY" || date_string_format == "MM-DD-YYYY" || date_string_format == "MM/DD/YYYY") {
5259 month = std::stoi(datestr.substr(0, 2));
5260 day = std::stoi(datestr.substr(2, 2));
5261 year = std::stoi(datestr.substr(4, 4));
5262 } else if (date_string_format == "YYYYDDMM") {
5263 year = std::stoi(datestr.substr(0, 4));
5264 day = std::stoi(datestr.substr(4, 2));
5265 month = std::stoi(datestr.substr(6, 2));
5266 } else {
5267 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): Invalid date string format '" + date_string_format + "' for compact date on line " + std::to_string(row) + " of file " + data_file + ".");
5268 }
5269 if (year < 1000 || month < 1 || month > 12 || day < 1 || day > 31) {
5270 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): Could not parse compact date string on line " + std::to_string(row) + " of file " + data_file + ".");
5271 }
5272 return make_Date(day, month, year);
5273 }
5274 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): Could not parse date string on line " + std::to_string(row) + " of file " + data_file +
5275 ". Expected a delimited date (e.g., YYYY-MM-DD) or an 8-digit compact date (e.g., 20260203).");
5276 }
5277
5278 // Delimited date: try '-' then '/'
5279 std::vector<std::string> thisdatestr = separate_string_by_delimiter(datestr, "-");
5280 if (thisdatestr.size() != 3) {
5281 thisdatestr = separate_string_by_delimiter(datestr, "/");
5282 }
5283 if (thisdatestr.size() != 3) {
5284 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): Could not parse date string on line " + std::to_string(row) + " of file " + data_file +
5285 ". It should be in the format YYYY-MM-DD, delimited by either '-' or '/'.");
5286 }
5287
5288 std::vector<int> thisdate(3);
5289 for (int i = 0; i < 3; i++) {
5290 if (!parse_int(thisdatestr.at(i), thisdate.at(i))) {
5291 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): Could not parse date string on line " + std::to_string(row) + " of file " + data_file +
5292 ". It should be in the format YYYY-MM-DD, delimited by either '-' or '/'.");
5293 }
5294 }
5295
5296 int year, month, day;
5297 if (date_string_format == "YYYYMMDD" || date_string_format == "YYYY-MM-DD") {
5298 year = thisdate.at(0);
5299 month = thisdate.at(1);
5300 day = thisdate.at(2);
5301 } else if (date_string_format == "YYYYDDMM") {
5302 year = thisdate.at(0);
5303 month = thisdate.at(2);
5304 day = thisdate.at(1);
5305 } else if (date_string_format == "DDMMYYYY" || date_string_format == "DD-MM-YYYY" || date_string_format == "DD/MM/YYYY") {
5306 year = thisdate.at(2);
5307 month = thisdate.at(1);
5308 day = thisdate.at(0);
5309 } else if (date_string_format == "MMDDYYYY" || date_string_format == "MM-DD-YYYY" || date_string_format == "MM/DD/YYYY") {
5310 year = thisdate.at(2);
5311 month = thisdate.at(0);
5312 day = thisdate.at(1);
5313 } else {
5314 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): Invalid date string format in file " + data_file + ": " + date_string_format +
5315 ". Must be one of YYYYMMDD, YYYYDDMM, DDMMYYYY, MMDDYYYY (or with - or / delimiters, e.g. YYYY-MM-DD, DD/MM/YYYY).");
5316 }
5317
5318 if (year < 1000 || month < 1 || month > 12 || day < 1 || day > 31) {
5319 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): Could not parse date string on line " + std::to_string(row) + " of file " + data_file + ".");
5320 }
5321
5322 return make_Date(day, month, year);
5323 }
5324
5325 // Parse a time string: "HH", "HH:MM", or "HH:MM:SS"
5326 // Note: may return hour=24 (via direct struct assignment) for midnight rollover; caller must handle.
5327 Time parseTimeString(const std::string &timestr, size_t row, const std::string &data_file) {
5328 std::string trimmed = trim_whitespace(timestr);
5329
5330 std::vector<std::string> parts = separate_string_by_delimiter(trimmed, ":");
5331 int hour = 0, minute = 0, second = 0;
5332
5333 if (parts.size() == 1) {
5334 // Integer hour
5335 if (!parse_int(parts.at(0), hour)) {
5336 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): Could not parse time string '" + timestr + "' on line " + std::to_string(row) + " of file " + data_file + ".");
5337 }
5338 // Handle HHMM format (e.g., 1300)
5339 if (hour > 24) {
5340 int hr_min = hour;
5341 hour = hr_min / 100;
5342 minute = hr_min - hour * 100;
5343 }
5344 } else if (parts.size() == 2) {
5345 if (!parse_int(parts.at(0), hour) || !parse_int(parts.at(1), minute)) {
5346 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): Could not parse time string '" + timestr + "' on line " + std::to_string(row) + " of file " + data_file + ".");
5347 }
5348 } else if (parts.size() == 3) {
5349 if (!parse_int(parts.at(0), hour) || !parse_int(parts.at(1), minute)) {
5350 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): Could not parse time string '" + timestr + "' on line " + std::to_string(row) + " of file " + data_file + ".");
5351 }
5352 // Handle fractional seconds by truncating at '.'
5353 std::string sec_str = parts.at(2);
5354 size_t dot_pos = sec_str.find('.');
5355 if (dot_pos != std::string::npos) {
5356 sec_str = sec_str.substr(0, dot_pos);
5357 }
5358 if (!parse_int(sec_str, second)) {
5359 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): Could not parse time string '" + timestr + "' on line " + std::to_string(row) + " of file " + data_file + ".");
5360 }
5361 } else {
5362 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): Could not parse time string '" + timestr + "' on line " + std::to_string(row) + " of file " + data_file + ".");
5363 }
5364
5365 // Handle hour=24 by directly setting struct fields (make_Time validates hour < 24)
5366 if (hour == 24) {
5367 Time t;
5368 t.hour = 24;
5369 t.minute = minute;
5370 t.second = second;
5371 return t;
5372 }
5373
5374 return make_Time(hour, minute, second);
5375 }
5376
5377 // Parse an ISO-8601 datetime string (e.g., "2026-02-03T10:00:00Z" or "2026-02-03T02:00:00-08:00")
5378 void parseISO8601(const std::string &datetimestr, Date &date, Time &time, float &utc_offset, size_t row, const std::string &data_file) {
5379 utc_offset = NAN;
5380
5381 size_t t_pos = datetimestr.find('T');
5382 if (t_pos == std::string::npos) {
5383 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): ISO-8601 datetime string '" + datetimestr + "' on line " + std::to_string(row) + " of file " + data_file + " does not contain 'T' separator.");
5384 }
5385
5386 // Parse date part (always YYYY-MM-DD)
5387 std::string date_part = datetimestr.substr(0, t_pos);
5388 std::vector<std::string> date_parts = separate_string_by_delimiter(date_part, "-");
5389 if (date_parts.size() != 3) {
5390 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): Could not parse date portion of ISO-8601 string '" + datetimestr + "' on line " + std::to_string(row) + " of file " + data_file + ".");
5391 }
5392 int year, month, day;
5393 if (!parse_int(date_parts.at(0), year) || !parse_int(date_parts.at(1), month) || !parse_int(date_parts.at(2), day)) {
5394 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): Could not parse date portion of ISO-8601 string '" + datetimestr + "' on line " + std::to_string(row) + " of file " + data_file + ".");
5395 }
5396 date = make_Date(day, month, year);
5397
5398 // Parse time part + optional timezone
5399 std::string time_tz = datetimestr.substr(t_pos + 1);
5400
5401 // Strip and parse timezone suffix
5402 std::string time_part;
5403 if (time_tz.back() == 'Z' || time_tz.back() == 'z') {
5404 time_part = time_tz.substr(0, time_tz.size() - 1);
5405 utc_offset = 0.0f; // UTC → Helios convention: +West, so UTC = 0
5406 } else {
5407 // Look for +/- timezone offset (e.g., +05:30, -08:00)
5408 // Search from after the hour portion to avoid matching a negative hour (shouldn't happen in ISO-8601 time)
5409 size_t tz_pos = std::string::npos;
5410 for (size_t i = 1; i < time_tz.size(); i++) {
5411 if (time_tz[i] == '+' || time_tz[i] == '-') {
5412 tz_pos = i;
5413 // Keep searching — we want the last +/- that's part of timezone, not inside time
5414 // Actually for ISO-8601, the timezone offset is always at the end, so we want the last occurrence
5415 }
5416 }
5417 if (tz_pos != std::string::npos) {
5418 time_part = time_tz.substr(0, tz_pos);
5419 std::string tz_str = time_tz.substr(tz_pos); // e.g., "-08:00" or "+05:30"
5420 char tz_sign = tz_str[0];
5421 std::string tz_num = tz_str.substr(1);
5422 std::vector<std::string> tz_parts = separate_string_by_delimiter(tz_num, ":");
5423 int tz_hours = 0, tz_minutes = 0;
5424 if (!tz_parts.empty()) parse_int(tz_parts.at(0), tz_hours);
5425 if (tz_parts.size() > 1) parse_int(tz_parts.at(1), tz_minutes);
5426 float iso_offset_hours = static_cast<float>(tz_hours) + static_cast<float>(tz_minutes) / 60.0f;
5427 if (tz_sign == '-') iso_offset_hours = -iso_offset_hours;
5428 // Helios convention: UTC_offset is +West. ISO convention: +East.
5429 // So ISO -08:00 (Pacific) → Helios +8, ISO +05:30 (India) → Helios -5.5
5430 utc_offset = -iso_offset_hours;
5431 } else {
5432 time_part = time_tz; // No timezone info
5433 }
5434 }
5435
5436 // Truncate fractional seconds
5437 size_t dot_pos = time_part.find('.');
5438 if (dot_pos != std::string::npos) {
5439 time_part = time_part.substr(0, dot_pos);
5440 }
5441
5442 // Parse the time portion
5443 time = parseTimeString(time_part, row, data_file);
5444 }
5445
5446 // Dispatch combined datetime string parsing based on format
5447 void parseDatetimeString(const std::string &datetimestr, const std::string &date_string_format,
5448 Date &date, Time &time, float &utc_offset, size_t row, const std::string &data_file) {
5449 utc_offset = NAN;
5450
5451 if (date_string_format == "ISO8601") {
5452 parseISO8601(datetimestr, date, time, utc_offset, row, data_file);
5453 return;
5454 }
5455
5456 if (date_string_format == "YYYYMMDDHH") {
5457 if (datetimestr.size() < 10) {
5458 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): YYYYMMDDHH datetime string '" + datetimestr + "' on line " + std::to_string(row) + " of file " + data_file + " is too short.");
5459 }
5460 int year = std::stoi(datetimestr.substr(0, 4));
5461 int month = std::stoi(datetimestr.substr(4, 2));
5462 int day = std::stoi(datetimestr.substr(6, 2));
5463 int hour = std::stoi(datetimestr.substr(8, 2));
5464 date = make_Date(day, month, year);
5465 time = make_Time(hour, 0, 0);
5466 return;
5467 }
5468
5469 if (date_string_format == "YYYYMMDDHHMM") {
5470 if (datetimestr.size() < 12) {
5471 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): YYYYMMDDHHMM datetime string '" + datetimestr + "' on line " + std::to_string(row) + " of file " + data_file + " is too short.");
5472 }
5473 int year = std::stoi(datetimestr.substr(0, 4));
5474 int month = std::stoi(datetimestr.substr(4, 2));
5475 int day = std::stoi(datetimestr.substr(6, 2));
5476 int hour = std::stoi(datetimestr.substr(8, 2));
5477 int minute = std::stoi(datetimestr.substr(10, 2));
5478 date = make_Date(day, month, year);
5479 time = make_Time(hour, minute, 0);
5480 return;
5481 }
5482
5483 // Formats with space separator: "YYYY-MM-DD HH:MM", "DD/MM/YYYY HH:MM", etc.
5484 // The space has already been rejoined by the caller, so split at space
5485 size_t space_pos = datetimestr.find(' ');
5486 if (space_pos != std::string::npos) {
5487 std::string date_part = datetimestr.substr(0, space_pos);
5488 std::string time_part = datetimestr.substr(space_pos + 1);
5489
5490 // Determine the date format portion (strip the time portion from format)
5491 std::string date_format;
5492 size_t fmt_space = date_string_format.find(' ');
5493 if (fmt_space != std::string::npos) {
5494 date_format = date_string_format.substr(0, fmt_space);
5495 } else {
5496 date_format = date_string_format;
5497 }
5498
5499 // Normalize date format: "YYYY-MM-DD" → "YYYYMMDD", "DD/MM/YYYY" → "DDMMYYYY", etc.
5500 // parseDateString handles both delimited and synonym formats
5501 date = parseDateString(date_part, date_format, row, data_file);
5502 time = parseTimeString(time_part, row, data_file);
5503 return;
5504 }
5505
5506 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): Could not parse datetime string '" + datetimestr + "' with format '" + date_string_format + "' on line " + std::to_string(row) + " of file " + data_file + ".");
5507 }
5508
5509 // Check if a datetime format string contains a space (i.e., date and time parts separated by space)
5510 bool datetimeFormatHasSpace(const std::string &format) {
5511 return format.find(' ') != std::string::npos;
5512 }
5513
5514} // anonymous namespace
5515
5516void Context::loadTabularTimeseriesData(const std::string &data_file, const std::vector<std::string> &col_labels, const std::string &a_delimeter, const std::string &a_date_string_format, uint headerlines) {
5517 // Resolve file path using project-based resolution
5518 std::filesystem::path resolved_path = resolveProjectFile(data_file);
5519 std::string resolved_filename = resolved_path.string();
5520
5521 std::ifstream datafile(resolved_filename); // open the file
5522
5523 if (!datafile.is_open()) { // check that file exists
5524 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): Weather data file '" + data_file + "' does not exist.");
5525 }
5526
5527 int yearcol = -1;
5528 int DOYcol = -1;
5529 int datestrcol = -1;
5530 int datetimecol = -1;
5531 int hourcol = -1;
5532 int minutecol = -1;
5533 int secondcol = -1;
5534 int timecol = -1;
5535 std::map<std::string, int> datacols;
5536
5537 size_t Ncolumns = 0;
5538
5539 size_t row = headerlines;
5540
5541 std::vector<std::string> column_labels = col_labels;
5542 std::string delimiter = a_delimeter;
5543 std::string date_string_format = a_date_string_format;
5544
5545 // pre-defined labels for CIMIS weather data files
5546 if (col_labels.size() == 1 && (col_labels.front() == "CIMIS" || col_labels.front() == "cimis")) {
5547 column_labels = {
5548 "", "", "", "date", "hour", "DOY", "ETo", "", "precipitation", "", "net_radiation", "", "vapor_pressure", "", "air_temperature", "", "air_humidity", "", "dew_point", "", "wind_speed", "", "wind_direction", "", "soil_temperature", ""};
5549 headerlines = 1;
5550 delimiter = ",";
5551 date_string_format = "MMDDYYYY";
5552 }
5553
5554 // If user specified column labels as an argument, parse them
5555 if (!column_labels.empty()) {
5556 int col = 0;
5557 for (auto &label: column_labels) {
5558 if (label == "year" || label == "Year") {
5559 yearcol = col;
5560 } else if (label == "DOY" || label == "Jul") {
5561 DOYcol = col;
5562 } else if (label == "date" || label == "Date") {
5563 datestrcol = col;
5564 } else if (label == "datetime" || label == "Datetime" || label == "DateTime") {
5565 datetimecol = col;
5566 } else if (label == "hour" || label == "Hour") {
5567 hourcol = col;
5568 } else if (label == "minute" || label == "Minute") {
5569 minutecol = col;
5570 } else if (label == "second" || label == "Second") {
5571 secondcol = col;
5572 } else if (label == "time" || label == "Time") {
5573 timecol = col;
5574 } else if (!label.empty()) {
5575 if (datacols.find(label) == datacols.end()) {
5576 datacols[label] = col;
5577 } else {
5578 datacols[label + "_dup"] = col;
5579 }
5580 }
5581
5582 col++;
5583 }
5584
5585 Ncolumns = column_labels.size();
5586
5587 // If column labels were not provided, read the first line of the text file and parse it for labels
5588 } else {
5589 if (headerlines == 0) {
5590 std::cerr << "WARNING (Context::loadTabularTimeseriesData): "
5591 "headerlines"
5592 " argument was specified as zero, and no column label information was given. Attempting to read the first line to see if it contains label information."
5593 << std::endl;
5594 headerlines++;
5595 }
5596
5597 std::string line;
5598 if (std::getline(datafile, line)) {
5599 std::vector<std::string> line_parsed = separate_string_by_delimiter(line, delimiter);
5600
5601 if (line_parsed.empty()) {
5602 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): Attempted to parse first line of file for column labels, but it did not contain the specified delimiter.");
5603 }
5604
5605 Ncolumns = line_parsed.size();
5606
5607 for (int col = 0; col < Ncolumns; col++) {
5608 const std::string &label = line_parsed.at(col);
5609
5610 if (label == "year" || label == "Year") {
5611 yearcol = col;
5612 } else if (label == "DOY" || label == "Jul") {
5613 DOYcol = col;
5614 } else if (label == "date" || label == "Date") {
5615 datestrcol = col;
5616 } else if (label == "datetime" || label == "Datetime" || label == "DateTime") {
5617 datetimecol = col;
5618 } else if (label == "hour" || label == "Hour") {
5619 hourcol = col;
5620 } else if (label == "minute" || label == "Minute") {
5621 minutecol = col;
5622 } else if (label == "second" || label == "Second") {
5623 secondcol = col;
5624 } else if (label == "time" || label == "Time") {
5625 timecol = col;
5626 } else if (!label.empty()) {
5627 if (datacols.find(label) == datacols.end()) {
5628 datacols[label] = col;
5629 } else {
5630 datacols[label + "_dup"] = col;
5631 }
5632 }
5633 }
5634
5635 headerlines--;
5636 } else {
5637 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): Attempted to parse first line of file for column labels, but read failed.");
5638 }
5639
5640 if (yearcol == -1 && DOYcol == -1 && datestrcol == -1 && datetimecol == -1) {
5641 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): Attempted to parse first line of file for column labels, but could not find valid label information.");
5642 }
5643 }
5644
5645 // Validate column combinations
5646 bool has_date = (datestrcol >= 0) || (yearcol >= 0 && DOYcol >= 0);
5647 bool has_time = (hourcol >= 0) || (timecol >= 0);
5648 bool has_datetime = (datetimecol >= 0);
5649
5650 if (has_datetime && datestrcol >= 0) {
5651 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): Cannot specify both 'datetime' and 'date' columns. Use 'datetime' for combined date+time, or 'date' + 'hour'/'time' for separate columns.");
5652 }
5653 if (has_datetime && hourcol >= 0) {
5654 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): Cannot specify both 'datetime' and 'hour' columns. Use 'datetime' for combined date+time, or 'date' + 'hour' for separate columns.");
5655 }
5656 if (has_datetime && timecol >= 0) {
5657 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): Cannot specify both 'datetime' and 'time' columns. The 'datetime' column already includes time information.");
5658 }
5659 if (!has_datetime && !has_date) {
5660 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): The date must be specified by a column labeled 'datetime', 'date', or by two columns labeled 'year' and 'DOY'.");
5661 }
5662 if (!has_datetime && !has_time) {
5663 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): The time must be specified by a column labeled 'datetime', 'hour', or 'time'.");
5664 }
5665 if (datacols.empty()) {
5666 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): No columns were found containing data variables (e.g., temperature, humidity, wind speed).");
5667 }
5668
5669 // Check if datetime format has a space — we may need to rejoin split columns
5670 bool datetime_format_has_space = has_datetime && datetimeFormatHasSpace(date_string_format);
5671
5672 std::string line;
5673
5674 // skip header lines
5675 // note: if we read labels from the first header line above, we don't need to skip another line
5676 for (int i = 0; i < headerlines; i++) {
5677 std::getline(datafile, line);
5678 }
5679
5680 bool utc_offset_set = false;
5681
5682 while (std::getline(datafile, line)) { // loop through file to read data
5683 row++;
5684
5685 if (trim_whitespace(line).empty() && row > 1) {
5686 break;
5687 }
5688
5689 // separate the line by delimiter
5690 std::vector<std::string> line_separated = separate_string_by_delimiter(line, delimiter);
5691
5692 // Handle space-delimited datetime auto-rejoin: if the datetime format contains a space
5693 // (e.g., "YYYY-MM-DD HH:MM"), the space delimiter will split the datetime into two columns.
5694 // Rejoin them here.
5695 if (datetime_format_has_space && datetimecol >= 0 && line_separated.size() == Ncolumns + 1 && datetimecol + 1 < static_cast<int>(line_separated.size())) {
5696 line_separated[datetimecol] = line_separated[datetimecol] + " " + line_separated[datetimecol + 1];
5697 line_separated.erase(line_separated.begin() + datetimecol + 1);
5698 }
5699
5700 if (line_separated.size() != Ncolumns) {
5701 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): Line " + std::to_string(row) + " had " + std::to_string(line_separated.size()) + " columns, but was expecting " + std::to_string(Ncolumns));
5702 }
5703
5704 Date date;
5705 Time time;
5706 float parsed_utc_offset = NAN;
5707
5708 if (datetimecol >= 0) {
5709 // Combined datetime column
5710 parseDatetimeString(line_separated.at(datetimecol), date_string_format,
5711 date, time, parsed_utc_offset, row, data_file);
5712 } else {
5713 // Separate date + time columns
5714 if (yearcol >= 0 && DOYcol >= 0) {
5715 int DOY;
5716 parse_int(line_separated.at(DOYcol), DOY);
5717 if (DOY < 1 || DOY > 366) {
5718 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): Invalid date specified on line " + std::to_string(row) + ".");
5719 }
5720 int year;
5721 parse_int(line_separated.at(yearcol), year);
5722 if (year < 1000) {
5723 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): Invalid year specified on line " + std::to_string(row) + ".");
5724 }
5725 date = make_Date(DOY, year);
5726 } else if (datestrcol >= 0) {
5727 date = parseDateString(line_separated.at(datestrcol), date_string_format, row, data_file);
5728 }
5729
5730 if (timecol >= 0) {
5731 time = parseTimeString(line_separated.at(timecol), row, data_file);
5732 } else if (hourcol >= 0) {
5733 int hour = 0;
5734 int minute = 0;
5735 int second = 0;
5736
5737 if (!parse_int(line_separated.at(hourcol), hour)) {
5738 helios_runtime_error("ERROR (Context::loadTabularTimeseriesData): Could not parse hour string on line " + std::to_string(row) + " of file " + data_file + ".");
5739 }
5740 if (hour > 24 && minutecol < 0 && secondcol < 0) {
5741 int hr_min = hour;
5742 hour = hr_min / 100;
5743 minute = hr_min - hour * 100;
5744 }
5745 if (hour == 24) {
5746 hour = 0;
5747 date.incrementDay();
5748 }
5749 if (minutecol >= 0) {
5750 if (!parse_int(line_separated.at(minutecol), minute)) {
5751 minute = 0;
5752 std::cout << "WARNING (Context::loadTabularTimeseriesData): Could not parse minute string on line " << row << " of file " << data_file << ". Setting minute equal to 0." << std::endl;
5753 }
5754 }
5755 if (secondcol >= 0) {
5756 if (!parse_int(line_separated.at(secondcol), second)) {
5757 second = 0;
5758 std::cout << "WARNING (Context::loadTabularTimeseriesData): Could not parse second string on line " << row << " of file " << data_file << ". Setting second equal to 0." << std::endl;
5759 }
5760 }
5761 time = make_Time(hour, minute, second);
5762 }
5763 }
5764
5765 // Handle hour=24 rollover
5766 if (time.hour == 24) {
5767 time = make_Time(0, time.minute, time.second);
5768 date.incrementDay();
5769 }
5770
5771 // Set UTC offset from ISO-8601 if parsed
5772 if (!std::isnan(parsed_utc_offset)) {
5773 if (!utc_offset_set) {
5774 Location loc = getLocation();
5775 loc.UTC_offset = parsed_utc_offset;
5776 setLocation(loc);
5777 utc_offset_set = true;
5778 }
5779 }
5780
5781 // compile data values
5782 for (auto &dat: datacols) {
5783 std::string label = dat.first;
5784 int col = dat.second;
5785
5786 float dataval;
5787 if (!parse_float(line_separated.at(col), dataval)) {
5788 std::cout << "WARNING (Context::loadTabularTimeseriesData): Failed to parse data value as float on line "
5789 << row << ", column " << col + 1 << " of file " << data_file << ". Skipping this value..." << std::endl;
5790 continue;
5791 }
5792
5793 if (label == "air_humidity" && col_labels.size() == 1 && (col_labels.front() == "CIMIS" || col_labels.front() == "cimis")) {
5794 dataval = dataval / 100.f;
5795 }
5796
5797 addTimeseriesData(label.c_str(), dataval, date, time);
5798 }
5799 }
5800
5801 datafile.close();
5802}