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