13namespace fs = std::filesystem;
14using json = nlohmann::json;
18static void ParseVisibleWhen(
const json& vw,
19 bool& has_visible_when,
21 std::vector<VisibleWhenCondition>& conditions,
24 has_visible_when =
true;
30 has_visible_when =
false;
36 if (obj.contains(
"parameter") && obj[
"parameter"].is_string()) {
37 c.source_type =
"parameter";
38 c.source_name = obj[
"parameter"].get<std::string>();
39 }
else if ((obj.contains(
"item") && obj[
"item"].is_string()) ||
40 (obj.contains(
"question") && obj[
"question"].is_string())) {
41 c.source_type =
"item";
42 c.source_name = obj.contains(
"item") ? obj[
"item"].get<std::string>() : obj[
"question"].get<std::string>();
46 if (obj.contains(
"operator") && obj[
"operator"].is_string()) {
47 c.op = obj[
"operator"].get<std::string>();
49 if (obj.contains(
"value")) {
50 if (obj[
"value"].is_string()) {
51 c.value = obj[
"value"].get<std::string>();
53 }
else if (obj[
"value"].is_array()) {
55 for (
const auto& v : obj[
"value"]) {
57 c.values.push_back(v.get<std::string>());
59 c.values.push_back(v.dump());
64 c.value = obj[
"value"].dump();
72 if (vw.contains(
"parameter") || vw.contains(
"item") || vw.contains(
"question")) {
74 if (parseSimple(vw, c)) {
75 conditions.push_back(c);
82 if (vw.contains(
"all")) {
85 }
else if (vw.contains(
"any")) {
94 if (!vw[key].is_array()) {
100 for (
const auto& child : vw[key]) {
101 if (child.contains(
"all") || child.contains(
"any")) {
107 if (parseSimple(child, c)) {
108 conditions.push_back(c);
118static json SerializeVisibleWhen(
const std::string& logic,
119 const std::vector<VisibleWhenCondition>& conditions)
123 if (c.source_type ==
"item") {
124 obj[
"item"] = c.source_name;
126 obj[
"parameter"] = c.source_name;
128 obj[
"operator"] = c.op;
130 json arr = json::array();
131 for (
const auto& v : c.values) {
136 obj[
"value"] = c.value;
141 if (conditions.size() == 1) {
142 return condToJson(conditions[0]);
145 json arr = json::array();
146 for (
const auto& c : conditions) {
147 arr.push_back(condToJson(c));
149 return json({{logic, arr}});
153 : mDefaultRequired(-1)
155 , mSourceIsOSD(false)
159 mDataOutput.
report_file =
"{code}-report-{subnum}.html";
168 auto scale = std::make_shared<ScaleDefinition>();
169 scale->mScaleInfo.code = code;
170 scale->mScaleInfo.name = code;
173 scale->mTranslations[
"en"][
"question_head"] =
"Please answer the following questions:";
174 scale->mTranslations[
"en"][
"debrief"] =
"Thank you for completing this questionnaire.";
176 scale->mDirty =
true;
182 auto scale = std::make_shared<ScaleDefinition>();
183 if (!scale->LoadDefinitionJSON(jsonPath)) {
186 scale->mDirty =
false;
192 auto scale = std::make_shared<ScaleDefinition>();
193 if (!scale->LoadFromOSDBundlePath(osdPath)) {
196 scale->mDirty =
false;
203 std::string scaleDir = basePath +
"/" + scaleCode;
204 std::string defPath = scaleDir +
"/" + scaleCode +
".json";
205 std::string osdPath = scaleDir +
"/" + scaleCode +
".osd";
207 if (fs::exists(defPath)) {
208 if (!LoadDefinitionJSON(defPath)) {
215 if (fs::exists(osdPath)) {
217 std::ifstream osdFile(osdPath);
218 if (osdFile.is_open()) {
221 if (bundle.contains(
"definition") && bundle[
"definition"].contains(
"parameters")) {
222 for (
auto& [key, value] : bundle[
"definition"][
"parameters"].items()) {
224 if (value.contains(
"type")) param.
type = value[
"type"];
225 if (value.contains(
"default")) param.
defaultValue = value[
"default"].is_string()
226 ? value[
"default"].get<std::string>()
227 : value[
"default"].dump();
228 if (value.contains(
"description")) param.
description = value[
"description"];
229 if (value.contains(
"options")) {
230 for (
const auto& opt : value[
"options"]) {
231 param.
options.push_back(opt.is_string() ? opt.get<std::string>() : opt.dump());
235 if (mParameters.find(key) == mParameters.end()) {
236 mParameters[key] = param;
245 }
else if (fs::exists(osdPath)) {
247 if (!LoadFromOSDBundlePath(osdPath)) {
258 std::string pblPrefix = scaleCode +
".pbl-";
259 std::string defFilename = scaleCode +
".json";
262 if (fs::exists(scaleDir)) {
263 for (
const auto& entry : fs::directory_iterator(scaleDir)) {
264 if (entry.is_regular_file()) {
265 std::string filename = entry.path().filename().string();
268 if (filename == defFilename)
continue;
271 if (filename.find(pblPrefix) == 0 &&
272 filename.size() >= 5 &&
273 filename.substr(filename.size() - 5) ==
".json") {
274 std::string lang = filename.substr(pblPrefix.length());
275 lang = lang.substr(0, lang.length() - 5);
276 LoadTranslationJSON(entry.path().string(), lang);
281 std::string openPrefix = scaleCode +
".";
282 if (filename.find(openPrefix) == 0 &&
283 filename.size() >= 5 &&
284 filename.substr(filename.size() - 5) ==
".json") {
285 std::string lang = filename.substr(openPrefix.length());
286 lang = lang.substr(0, lang.length() - 5);
287 if (!lang.empty() && lang.find(
'.') == std::string::npos) {
288 LoadTranslationJSON(entry.path().string(), lang);
294 }
catch (
const std::exception& e) {
304 if (SaveDefinitionJSON(jsonPath)) {
314 std::string defFile = definitionsPath +
"/" + mScaleInfo.
code +
".json";
315 if (!SaveDefinitionJSON(defFile)) {
320 for (
const auto& [lang, translations] : mTranslations) {
321 std::string transFile = translationsPath +
"/" + mScaleInfo.
code +
"." + lang +
".json";
322 if (!SaveTranslationJSON(transFile, lang)) {
337 if (!BuildDefinitionJSONObject(definition)) {
338 printf(
"ExportToOSD: failed to build definition JSON\n");
343 for (
const auto& [lang, transMap] : mTranslations) {
345 for (
const auto& [key, value] : transMap) {
346 transJson[key] = value;
348 translations[lang] = transJson;
352 bundle[
"osd_version"] =
"1.0";
353 bundle[
"definition"] = definition;
354 bundle[
"translations"] = translations;
356 std::string osdFile = outputDir +
"/" + mScaleInfo.
code +
".osd";
357 std::ofstream f(osdFile);
358 if (!f.is_open())
return false;
361 }
catch (
const std::exception& e) {
362 printf(
"ExportToOSD error: %s\n", e.what());
367bool ScaleDefinition::LoadDefinitionJSON(
const std::string& jsonPath)
370 std::ifstream file(jsonPath);
371 if (!file.is_open()) {
376 return ParseDefinitionFromJSON(j);
377 }
catch (
const std::exception& e) {
378 std::cerr <<
"Error loading scale definition: " << e.what() << std::endl;
383bool ScaleDefinition::ParseDefinitionFromJSON(
const json& j)
391 if (j.contains(
"scale_info")) {
392 auto& info = j[
"scale_info"];
393 if (info.contains(
"name")) mScaleInfo.
name = info[
"name"];
394 if (info.contains(
"code")) mScaleInfo.
code = info[
"code"];
395 if (info.contains(
"abbreviation")) mScaleInfo.
abbreviation = info[
"abbreviation"];
396 if (info.contains(
"description")) mScaleInfo.
description = info[
"description"];
397 if (info.contains(
"citation")) mScaleInfo.
citation = info[
"citation"];
398 if (info.contains(
"license")) mScaleInfo.
license = info[
"license"];
399 if (info.contains(
"license_explanation")) mScaleInfo.
license_explanation = info[
"license_explanation"];
400 if (info.contains(
"license_url")) mScaleInfo.
license_url = info[
"license_url"];
401 if (info.contains(
"version")) mScaleInfo.
version = info[
"version"];
402 if (info.contains(
"url")) mScaleInfo.
url = info[
"url"];
403 if (info.contains(
"domain")) mScaleInfo.
domain = info[
"domain"];
407 if (j.contains(
"parameters")) {
409 for (
auto& [key, value] : j[
"parameters"].items()) {
411 if (value.contains(
"type")) param.
type = value[
"type"];
412 if (value.contains(
"default")) {
413 if (value[
"default"].is_string()) {
419 if (value.contains(
"description")) param.
description = value[
"description"];
420 if (value.contains(
"options") && value[
"options"].is_array()) {
421 for (
const auto& opt : value[
"options"]) {
422 if (opt.is_string()) {
423 param.
options.push_back(opt.get<std::string>());
425 param.
options.push_back(opt.dump());
429 mParameters[key] = param;
434 if (j.contains(
"likert_options")) {
435 auto& lo = j[
"likert_options"];
436 if (lo.contains(
"points")) mLikertOptions.
points = lo[
"points"];
437 if (lo.contains(
"min")) mLikertOptions.
min = lo[
"min"];
438 if (lo.contains(
"max")) mLikertOptions.
max = lo[
"max"];
439 if (lo.contains(
"question_head")) mLikertOptions.
question_head = lo[
"question_head"];
440 if (lo.contains(
"labels")) {
441 mLikertOptions.
labels = lo[
"labels"].get<std::vector<std::string>>();
446 if (j.contains(
"default_required")) {
447 if (j[
"default_required"].is_boolean()) {
448 mDefaultRequired = j[
"default_required"].get<
bool>() ? 1 : 0;
453 if (j.contains(
"dimensions")) {
455 for (
const auto& dj : j[
"dimensions"]) {
457 if (dj.contains(
"id")) d.
id = dj[
"id"];
458 if (dj.contains(
"name")) d.
name = dj[
"name"];
459 if (dj.contains(
"abbreviation")) d.
abbreviation = dj[
"abbreviation"];
460 if (dj.contains(
"description")) d.
description = dj[
"description"];
461 if (dj.contains(
"enabled_param") && !dj[
"enabled_param"].is_null()) {
464 if (dj.contains(
"selectable") && dj[
"selectable"].is_boolean()) {
467 if (dj.contains(
"default_enabled") && dj[
"default_enabled"].is_boolean()) {
470 if (dj.contains(
"visible_when") && !dj[
"visible_when"].is_null()) {
471 bool is_complex =
false;
472 ParseVisibleWhen(dj[
"visible_when"],
479 mDimensions.push_back(d);
484 const std::string itemsKey = j.contains(
"items") ?
"items" : (j.contains(
"questions") ?
"questions" :
"");
485 if (!itemsKey.empty()) {
487 for (
const auto& qj : j[itemsKey]) {
489 if (qj.contains(
"id")) q.
id = qj[
"id"];
490 if (qj.contains(
"text_key")) q.
text_key = qj[
"text_key"];
491 if (qj.contains(
"type")) q.
type = qj[
"type"];
492 if (qj.contains(
"dimension") && !qj[
"dimension"].is_null()) {
495 if (qj.contains(
"coding")) q.
coding = qj[
"coding"];
496 if (qj.contains(
"random_group")) {
500 if (q.
type ==
"inst" || qj.contains(
"visible_when")) {
506 if (qj.contains(
"required")) {
507 if (qj[
"required"].is_boolean()) {
513 if (qj.contains(
"validation") && qj[
"validation"].is_object()) {
514 auto& vj = qj[
"validation"];
517 if (vj.contains(
"min_length")) val.
min_length = vj[
"min_length"].get<
int>();
518 if (vj.contains(
"max_length")) val.max_length = vj[
"max_length"].get<
int>();
519 if (vj.contains(
"min_length_error")) val.min_length_error = vj[
"min_length_error"].get<std::string>();
520 if (vj.contains(
"max_length_error")) val.max_length_error = vj[
"max_length_error"].get<std::string>();
521 if (vj.contains(
"min_words")) val.min_words = vj[
"min_words"].get<
int>();
522 if (vj.contains(
"max_words")) val.max_words = vj[
"max_words"].get<
int>();
523 if (vj.contains(
"min_words_error")) val.min_words_error = vj[
"min_words_error"].get<std::string>();
524 if (vj.contains(
"max_words_error")) val.max_words_error = vj[
"max_words_error"].get<std::string>();
525 if (vj.contains(
"number_min")) { val.number_min_set =
true; val.number_min = vj[
"number_min"].get<
double>(); }
526 if (vj.contains(
"number_max")) { val.number_max_set =
true; val.number_max = vj[
"number_max"].get<
double>(); }
527 if (vj.contains(
"number_min_error")) val.number_min_error = vj[
"number_min_error"].get<std::string>();
528 if (vj.contains(
"number_max_error")) val.number_max_error = vj[
"number_max_error"].get<std::string>();
529 if (vj.contains(
"pattern")) val.pattern = vj[
"pattern"].get<std::string>();
530 if (vj.contains(
"pattern_error")) val.pattern_error = vj[
"pattern_error"].get<std::string>();
531 if (vj.contains(
"min_selected")) val.min_selected = vj[
"min_selected"].get<
int>();
532 if (vj.contains(
"max_selected")) val.max_selected = vj[
"max_selected"].get<
int>();
533 if (vj.contains(
"min_selected_error")) val.min_selected_error = vj[
"min_selected_error"].get<std::string>();
534 if (vj.contains(
"max_selected_error")) val.max_selected_error = vj[
"max_selected_error"].get<std::string>();
536 if (vj.contains(
"type")) {
537 std::string legacyType = vj[
"type"].get<std::string>();
538 double legacyMin = vj.value(
"min", 0.0);
539 double legacyMax = vj.value(
"max", 0.0);
540 int legacyValue = vj.value(
"value", 0);
541 std::string legacyErrKey = vj.value(
"error_key", std::string(
""));
542 if (legacyType ==
"selection") {
543 if (legacyMin > 0) { val.min_selected = (int)legacyMin; val.min_selected_error = legacyErrKey; }
544 if (legacyMax > 0) { val.max_selected = (int)legacyMax; val.max_selected_error = legacyErrKey; }
545 }
else if (legacyType ==
"number") {
546 if (legacyMin != 0 || legacyMax != 0) {
547 val.number_min_set =
true; val.number_min = legacyMin; val.number_min_error = legacyErrKey;
548 val.number_max_set =
true; val.number_max = legacyMax; val.number_max_error = legacyErrKey;
550 }
else if (legacyType ==
"min_length") {
551 val.min_length = legacyValue; val.min_length_error = legacyErrKey;
552 }
else if (legacyType ==
"max_length") {
553 val.max_length = legacyValue; val.max_length_error = legacyErrKey;
554 }
else if (legacyType ==
"pattern") {
555 val.pattern = vj.value(
"regex", std::string(
"")); val.pattern_error = legacyErrKey;
561 if (qj.contains(
"visible_when") && !qj[
"visible_when"].is_null()) {
562 ParseVisibleWhen(qj[
"visible_when"],
570 if (qj.contains(
"likert_points")) q.
likert_points = qj[
"likert_points"];
571 if (qj.contains(
"likert_min")) q.
likert_min = qj[
"likert_min"];
572 if (qj.contains(
"likert_max")) q.
likert_max = qj[
"likert_max"];
573 if (qj.contains(
"likert_reverse")) q.
likert_reverse = qj[
"likert_reverse"].get<
bool>();
574 if (qj.contains(
"randomize_options")) q.
randomize_options = qj[
"randomize_options"].get<
bool>();
575 if (qj.contains(
"likert_labels") && qj[
"likert_labels"].is_array()) {
576 q.
likert_labels = qj[
"likert_labels"].get<std::vector<std::string>>();
578 if (qj.contains(
"min")) q.
min_value = qj[
"min"];
579 if (qj.contains(
"max")) q.
max_value = qj[
"max"];
580 if (qj.contains(
"left")) q.
left_label = qj[
"left"];
581 else if (qj.contains(
"min_label")) q.
left_label = qj[
"min_label"];
582 if (qj.contains(
"right")) q.
right_label = qj[
"right"];
583 else if (qj.contains(
"max_label")) q.
right_label = qj[
"max_label"];
585 if (qj.contains(
"anchors") && qj[
"anchors"].is_array()) {
587 for (
const auto& a : qj[
"anchors"]) {
589 if (a.contains(
"value")) va.
value = a[
"value"].get<
double>();
590 if (a.contains(
"label")) va.
label = a[
"label"];
594 if (qj.contains(
"options")) {
597 for (
const auto& opt : qj[
"options"]) {
598 if (opt.is_string()) {
599 q.
options.push_back(opt.get<std::string>());
600 }
else if (opt.is_object() && opt.contains(
"text_key")) {
601 q.
options.push_back(opt[
"text_key"].get<std::string>());
605 if (qj.contains(
"correct")) {
606 q.
correct = qj[
"correct"].get<std::vector<std::string>>();
608 if (qj.contains(
"image")) q.
image = qj[
"image"];
609 if (qj.contains(
"columns")) {
610 q.
columns = qj[
"columns"].get<std::vector<std::string>>();
612 if (qj.contains(
"rows")) {
613 q.
rows = qj[
"rows"].get<std::vector<std::string>>();
617 if (qj.contains(
"answer_alias") && qj[
"answer_alias"].is_string())
619 if (qj.contains(
"question_head") && qj[
"question_head"].is_string())
623 if (qj.contains(
"gate") && qj[
"gate"].is_object()) {
624 const auto& gj = qj[
"gate"];
627 if (gj.contains(
"operator")) q.
gate_operator = gj[
"operator"];
628 if (gj.contains(
"value") && gj[
"value"].is_number()) q.
gate_value = gj[
"value"].get<
double>();
633 if (q.
type ==
"section" && qj.contains(
"revisable") && qj[
"revisable"].is_boolean())
634 q.
revisable = qj[
"revisable"].get<
bool>();
637 if (q.
type ==
"section" && qj.contains(
"randomize") && qj[
"randomize"].is_object()) {
638 auto& r = qj[
"randomize"];
639 if (r.contains(
"method") && r[
"method"].is_string() && r[
"method"] ==
"shuffle")
641 if (r.contains(
"fixed") && r[
"fixed"].is_array())
642 for (
auto& f : r[
"fixed"]) if (f.is_string()) q.section_randomize_fixed.push_back(f);
645 mQuestions.push_back(q);
650 if (j.contains(
"scoring")) {
652 for (
auto& [key, value] : j[
"scoring"].items()) {
654 if (value.contains(
"method")) ds.
method = value[
"method"];
655 if (value.contains(
"items")) {
656 if (value[
"items"].is_array()) {
658 ds.
items = value[
"items"].get<std::vector<std::string>>();
659 for (
const auto&
id : ds.items) {
662 }
else if (value[
"items"].is_object()) {
664 for (
auto& [ikey, ival] : value[
"items"].items()) {
665 int coding = ival.get<
int>();
668 ds.
items.push_back(ikey);
673 if (value.contains(
"description")) ds.
description = value[
"description"];
674 if (value.contains(
"weights")) {
675 for (
auto& [wkey, wval] : value[
"weights"].items()) {
676 ds.
weights[wkey] = wval.get<
double>();
680 if (value.contains(
"item_coding")) {
681 for (
auto& [ikey, ival] : value[
"item_coding"].items()) {
684 if (ival.get<
int>() != 0) {
685 if (std::find(ds.
items.begin(), ds.
items.end(), ikey) == ds.
items.end()) {
686 ds.
items.push_back(ikey);
691 if (value.contains(
"correct_answers")) {
692 for (
auto& [cakey, caval] : value[
"correct_answers"].items()) {
696 if (value.contains(
"norms") && value[
"norms"].contains(
"thresholds")) {
697 for (
const auto& t : value[
"norms"][
"thresholds"]) {
699 if (t.contains(
"min")) nt.
min = t[
"min"].get<
double>();
700 if (t.contains(
"max")) nt.
max = t[
"max"].get<
double>();
701 if (t.contains(
"label_key")) nt.
label = t[
"label_key"].get<std::string>();
702 else if (t.contains(
"label")) nt.
label = t[
"label"].get<std::string>();
703 ds.
norms.push_back(nt);
706 if (value.contains(
"transform") && value[
"transform"].is_array()) {
707 for (
const auto& step : value[
"transform"]) {
709 if (step.contains(
"op")) ts.
op = step[
"op"].get<std::string>();
710 if (step.contains(
"value") && step[
"value"].is_number()) {
711 ts.
value = step[
"value"].get<
double>();
716 if (value.contains(
"scores")) {
717 ds.
scores = value[
"scores"].get<std::vector<std::string>>();
719 if (value.contains(
"value_map")) {
720 for (
auto& [vmkey, vmval] : value[
"value_map"].items()) {
721 if (vmval.is_array()) {
722 ds.
value_map[vmkey] = vmval.get<std::vector<double>>();
731 if (j.contains(
"computed")) {
733 for (
auto& [key, value] : j[
"computed"].items()) {
735 if (value.contains(
"expression")) cv.
expression = value[
"expression"];
736 if (value.contains(
"type")) cv.
type = value[
"type"];
737 if (value.contains(
"norms") && value[
"norms"].contains(
"thresholds")) {
738 for (
const auto& t : value[
"norms"][
"thresholds"]) {
740 if (t.contains(
"min")) nt.
min = t[
"min"].get<
double>();
741 if (t.contains(
"max")) nt.
max = t[
"max"].get<
double>();
742 if (t.contains(
"label_key")) nt.
label = t[
"label_key"].get<std::string>();
743 else if (t.contains(
"label")) nt.
label = t[
"label"].get<std::string>();
744 cv.
norms.push_back(nt);
752 if (j.contains(
"report")) {
753 auto& rj = j[
"report"];
754 if (rj.contains(
"template")) mReportConfig.
template_type = rj[
"template"];
755 if (rj.contains(
"include")) {
756 mReportConfig.
include = rj[
"include"].get<std::vector<std::string>>();
758 if (rj.contains(
"header")) mReportConfig.
header = rj[
"header"];
759 if (rj.contains(
"footer_refs")) {
760 mReportConfig.
footer_refs = rj[
"footer_refs"].get<std::vector<std::string>>();
765 if (j.contains(
"data_output")) {
766 auto& dj = j[
"data_output"];
767 if (dj.contains(
"individual_file")) mDataOutput.
individual_file = dj[
"individual_file"];
768 if (dj.contains(
"pooled_file")) mDataOutput.
pooled_file = dj[
"pooled_file"];
769 if (dj.contains(
"report_file")) mDataOutput.
report_file = dj[
"report_file"];
770 if (dj.contains(
"columns")) mDataOutput.
columns = dj[
"columns"];
771 if (dj.contains(
"pooled_columns")) mDataOutput.
pooled_columns = dj[
"pooled_columns"];
775 }
catch (
const std::exception& e) {
776 std::cerr <<
"Error parsing scale definition JSON: " << e.what() << std::endl;
781bool ScaleDefinition::LoadFromOSDBundlePath(
const std::string& osdPath)
784 std::ifstream file(osdPath);
785 if (!file.is_open()) {
791 if (!bundle.contains(
"definition")) {
794 if (!ParseDefinitionFromJSON(bundle[
"definition"])) {
799 if (bundle.contains(
"translations") && bundle[
"translations"].is_object()) {
800 for (
auto& [lang, trans] : bundle[
"translations"].items()) {
801 if (trans.is_object()) {
802 for (
auto& [key, value] : trans.items()) {
803 if (value.is_string()) {
804 mTranslations[lang][key] = value.get<std::string>();
813 }
catch (
const std::exception& e) {
814 std::cerr <<
"Error loading OSD bundle: " << e.what() << std::endl;
819bool ScaleDefinition::LoadTranslationJSON(
const std::string& jsonPath,
const std::string& language)
822 std::ifstream file(jsonPath);
823 if (!file.is_open()) {
831 for (
auto& [key, value] : j.items()) {
832 if (value.is_string()) {
833 mTranslations[language][key] = value.get<std::string>();
838 }
catch (
const std::exception& e) {
843bool ScaleDefinition::BuildDefinitionJSONObject(
json& outJSON)
const
848 json j = mRawDefinition;
854 {
"name", mScaleInfo.
name},
855 {
"code", mScaleInfo.
code},
859 {
"license", mScaleInfo.
license},
862 {
"version", mScaleInfo.
version},
863 {
"url", mScaleInfo.
url}
865 if (!mScaleInfo.
domain.empty()) {
866 j[
"scale_info"][
"domain"] = mScaleInfo.
domain;
870 if (!mParameters.empty()) {
871 j[
"parameters"] = json::object();
872 for (
const auto& [key, param] : mParameters) {
873 j[
"parameters"][key] = {
874 {
"type", param.
type},
879 j[
"parameters"][key][
"options"] = param.
options;
886 if (!j.contains(
"likert_options")) {
887 j[
"likert_options"] = json::object();
889 j[
"likert_options"][
"points"] = mLikertOptions.
points;
893 bool rawHadLikertOptions = mRawDefinition.contains(
"likert_options");
894 if (!rawHadLikertOptions || mRawDefinition[
"likert_options"].contains(
"question_head")) {
895 j[
"likert_options"][
"question_head"] = mLikertOptions.
question_head;
897 j[
"likert_options"][
"labels"] = mLikertOptions.
labels;
898 if (mLikertOptions.
min != -1) {
899 j[
"likert_options"][
"min"] = mLikertOptions.
min;
901 if (mLikertOptions.
max != -1) {
902 j[
"likert_options"][
"max"] = mLikertOptions.
max;
906 if (!mDimensions.empty()) {
907 j[
"dimensions"] = json::array();
908 for (
const auto& d : mDimensions) {
917 dj[
"selectable"] =
true;
923 j[
"dimensions"].push_back(dj);
931 std::map<std::string, json> rawQuestionMap;
932 const std::string rawKey = mRawDefinition.contains(
"items") ?
"items" :
933 (mRawDefinition.contains(
"questions") ?
"questions" :
"");
934 if (!rawKey.empty()) {
935 for (
const auto& rq : mRawDefinition[rawKey]) {
936 if (rq.contains(
"id")) {
937 rawQuestionMap[rq[
"id"].get<std::string>()] = rq;
942 j[
"items"] = json::array();
943 for (
const auto& q : mQuestions) {
946 auto it = rawQuestionMap.find(q.
id);
947 if (it != rawQuestionMap.end()) {
961 qj.erase(
"required");
968 if (val.min_length >= 0) { vj[
"min_length"] = val.
min_length;
if (!val.min_length_error.empty()) vj[
"min_length_error"] = val.min_length_error; }
969 if (val.max_length >= 0) { vj[
"max_length"] = val.max_length;
if (!val.max_length_error.empty()) vj[
"max_length_error"] = val.max_length_error; }
970 if (val.min_words >= 0) { vj[
"min_words"] = val.min_words;
if (!val.min_words_error.empty()) vj[
"min_words_error"] = val.min_words_error; }
971 if (val.max_words >= 0) { vj[
"max_words"] = val.max_words;
if (!val.max_words_error.empty()) vj[
"max_words_error"] = val.max_words_error; }
972 if (val.number_min_set) { vj[
"number_min"] = val.number_min;
if (!val.number_min_error.empty()) vj[
"number_min_error"] = val.number_min_error; }
973 if (val.number_max_set) { vj[
"number_max"] = val.number_max;
if (!val.number_max_error.empty()) vj[
"number_max_error"] = val.number_max_error; }
974 if (!val.pattern.empty()) { vj[
"pattern"] = val.pattern;
if (!val.pattern_error.empty()) vj[
"pattern_error"] = val.pattern_error; }
975 if (val.min_selected >= 0) { vj[
"min_selected"] = val.min_selected;
if (!val.min_selected_error.empty()) vj[
"min_selected_error"] = val.min_selected_error; }
976 if (val.max_selected >= 0) { vj[
"max_selected"] = val.max_selected;
if (!val.max_selected_error.empty()) vj[
"max_selected_error"] = val.max_selected_error; }
977 qj[
"validation"] = vj;
979 qj.erase(
"validation");
983 if (q.
type ==
"likert") {
994 qj[
"likert_reverse"] =
true;
1000 if (q.
type ==
"vas") {
1013 nlohmann::json anchorsArr = nlohmann::json::array();
1014 for (
const auto& a : q.vas_anchors) {
1015 anchorsArr.push_back({{
"value", a.value}, {
"label", a.label}});
1017 qj[
"anchors"] = anchorsArr;
1020 if (q.
type ==
"multi" || q.
type ==
"multicheck") {
1024 bool originalHasObjectOptions =
false;
1025 auto rawIt = rawQuestionMap.find(q.
id);
1026 if (rawIt != rawQuestionMap.end()
1027 && rawIt->second.contains(
"options")
1028 && rawIt->second[
"options"].is_array()
1029 && !rawIt->second[
"options"].empty()
1030 && rawIt->second[
"options"][0].is_object()) {
1031 originalHasObjectOptions =
true;
1033 if (!originalHasObjectOptions) {
1039 qj[
"randomize_options"] =
true;
1045 if (q.
type ==
"image" || q.
type ==
"imageresponse") {
1046 if (!q.
image.empty()) {
1047 qj[
"image"] = q.
image;
1050 if (q.
type ==
"grid") {
1054 if (!q.
rows.empty()) {
1055 qj[
"rows"] = q.
rows;
1061 qj.erase(
"visible_when");
1074 qj.erase(
"answer_alias");
1086 qj[
"gate"] = gateObj;
1093 qj[
"revisable"] =
false;
1095 qj.erase(
"revisable");
1100 rdm[
"method"] =
"shuffle";
1103 qj[
"randomize"] = rdm;
1105 qj.erase(
"randomize");
1108 j[
"items"].push_back(qj);
1113 if (mDefaultRequired >= 0) {
1114 j[
"default_required"] = (mDefaultRequired == 1);
1116 j.erase(
"default_required");
1120 if (!mScoring.empty()) {
1121 j[
"scoring"] = json::object();
1122 for (
const auto& [key, ds] : mScoring) {
1126 sj[
"method"] = ds.
method;
1129 bool allForward =
true;
1130 for (
const auto& [ikey, ival] : ds.item_coding) {
1131 if (ival != 1) { allForward =
false;
break; }
1136 sj[
"items"] = ds.
items;
1139 sj[
"items"] = json::object();
1141 for (
const auto&
id : ds.items) {
1143 sj[
"items"][id] = (it != ds.
item_coding.end()) ? it->second : 1;
1146 for (
const auto& [ikey, ival] : ds.item_coding) {
1147 if (std::find(ds.
items.begin(), ds.
items.end(), ikey) == ds.
items.end()) {
1148 sj[
"items"][ikey] = ival;
1157 sj[
"correct_answers"] = json::object();
1158 for (
const auto& [cakey, caval] : ds.correct_answers) {
1159 sj[
"correct_answers"][cakey] = caval;
1162 if (!ds.
norms.empty()) {
1163 nlohmann::json thresholdsArr = nlohmann::json::array();
1164 for (
const auto& t : ds.norms) {
1165 thresholdsArr.push_back({{
"min", t.min}, {
"max", t.max}, {
"label_key", t.label}});
1167 sj[
"norms"] = {{
"thresholds", thresholdsArr}};
1170 nlohmann::json tArr = nlohmann::json::array();
1171 for (
const auto& ts : ds.transform) {
1172 tArr.push_back({{
"op", ts.
op}, {
"value", ts.
value}});
1174 sj[
"transform"] = tArr;
1176 if (!ds.
scores.empty()) {
1177 sj[
"scores"] = ds.
scores;
1180 sj[
"value_map"] = json::object();
1181 for (
const auto& [vmkey, vmval] : ds.value_map) {
1182 sj[
"value_map"][vmkey] = vmval;
1185 j[
"scoring"][key] = sj;
1190 if (!mComputed.empty()) {
1191 j[
"computed"] = nlohmann::json::object();
1192 for (
const auto& [key, cv] : mComputed) {
1193 nlohmann::json cvj = {
1197 if (!cv.
norms.empty()) {
1198 nlohmann::json thresholdsArr = nlohmann::json::array();
1199 for (
const auto& t : cv.norms) {
1200 thresholdsArr.push_back({{
"min", t.min}, {
"max", t.max}, {
"label_key", t.label}});
1202 cvj[
"norms"] = {{
"thresholds", thresholdsArr}};
1204 j[
"computed"][key] = cvj;
1209 if (!mReportConfig.
template_type.empty() || mRawDefinition.contains(
"report")) {
1212 {
"include", mReportConfig.
include},
1213 {
"header", mReportConfig.
header},
1219 if (!mDataOutput.
individual_file.empty() || mRawDefinition.contains(
"data_output")) {
1220 j[
"data_output"] = {
1224 {
"columns", mDataOutput.
columns},
1233 outJSON = std::move(j);
1235 }
catch (
const std::exception& e) {
1236 std::cerr <<
"Error building scale definition JSON: " << e.what() << std::endl;
1241bool ScaleDefinition::SaveDefinitionJSON(
const std::string& jsonPath)
1245 if (!BuildDefinitionJSONObject(j)) {
1248 std::ofstream file(jsonPath);
1249 if (!file.is_open()) {
1255 }
catch (
const std::exception& e) {
1256 std::cerr <<
"Error saving scale definition: " << e.what() << std::endl;
1261bool ScaleDefinition::SaveTranslationJSON(
const std::string& jsonPath,
const std::string& language)
1264 if (mTranslations.find(language) == mTranslations.end()) {
1269 for (
const auto& [key, value] : mTranslations[language]) {
1273 std::ofstream file(jsonPath);
1274 if (!file.is_open()) {
1281 }
catch (
const std::exception& e) {
1288 mQuestions.push_back(question);
1294 if (index < 0) index = 0;
1295 if (index > (
int)mQuestions.size()) index = (int)mQuestions.size();
1296 mQuestions.insert(mQuestions.begin() + index, question);
1302 auto it = std::remove_if(mQuestions.begin(), mQuestions.end(),
1303 [&questionID](
const ScaleQuestion& q) { return q.id == questionID; });
1305 if (it != mQuestions.end()) {
1306 mQuestions.erase(it, mQuestions.end());
1313 if (fromIndex < 0 || fromIndex >= (
int)mQuestions.size() ||
1314 toIndex < 0 || toIndex >= (
int)mQuestions.size()) {
1318 if (fromIndex == toIndex) {
1323 mQuestions.erase(mQuestions.begin() + fromIndex);
1324 mQuestions.insert(mQuestions.begin() + toIndex, q);
1330 for (
auto& q : mQuestions) {
1331 if (q.
id == questionID) {
1340 mDimensions.push_back(dimension);
1346 auto it = std::remove_if(mDimensions.begin(), mDimensions.end(),
1347 [&dimensionID](
const ScaleDimension& d) { return d.id == dimensionID; });
1349 if (it != mDimensions.end()) {
1350 mDimensions.erase(it, mDimensions.end());
1357 for (
auto& d : mDimensions) {
1358 if (d.
id == dimensionID) {
1367 mTranslations[language][key] = value;
1373 mTranslations.erase(language);
1379 std::vector<std::string> languages;
1380 for (
const auto& [lang, _] : mTranslations) {
1381 languages.push_back(lang);
1388 auto langIt = mTranslations.find(language);
1389 if (langIt != mTranslations.end()) {
1390 auto keyIt = langIt->second.find(key);
1391 if (keyIt != langIt->second.end()) {
1392 return keyIt->second;
1403 if (mScaleInfo.
code.empty()) {
1404 result.
errors.push_back(
"Scale code is required");
1406 if (mScaleInfo.
name.empty()) {
1407 result.
errors.push_back(
"Scale name is required");
1411 if (mQuestions.empty()) {
1412 result.
errors.push_back(
"Scale must have at least one question");
1416 std::map<std::string, int> idCounts;
1417 for (
const auto& q : mQuestions) {
1419 result.
errors.push_back(
"Question missing ID");
1424 result.
warnings.push_back(
"Question " + q.
id +
" missing text_key");
1426 if (q.
type.empty()) {
1427 result.
errors.push_back(
"Question " + q.
id +
" missing type");
1431 for (
const auto& [
id,
count] : idCounts) {
1433 result.
errors.push_back(
"Duplicate question ID: " +
id);
1438 for (
const auto& dim : mDimensions) {
1439 if (dim.id.empty()) {
1440 result.
errors.push_back(
"Dimension missing ID");
1445 for (
const auto& [dimId, scoring] : mScoring) {
1446 for (
const auto& qid : scoring.items) {
1448 for (
const auto& q : mQuestions) {
1455 result.
warnings.push_back(
"Scoring " + dimId +
" references non-existent question: " + qid);
1468 std::stringstream ss;
1469 if (!result.
errors.empty()) {
1471 for (
const auto& err : result.
errors) {
1472 ss <<
" - " << err <<
"\n";
1476 ss <<
"Warnings:\n";
1477 for (
const auto& warn : result.
warnings) {
1478 ss <<
" - " << warn <<
"\n";
1482 errorOutput = ss.str();
bool Validate(std::string &errorOutput)
ScaleDimension * GetDimension(const std::string &dimensionID)
void InsertQuestion(int index, const ScaleQuestion &question)
std::string GetTranslation(const std::string &language, const std::string &key) const
void RemoveTranslation(const std::string &language)
bool LoadFromScalesDir(const std::string &basePath, const std::string &scaleCode)
void RemoveDimension(const std::string &dimensionID)
void AddDimension(const ScaleDimension &dimension)
std::vector< std::string > GetAvailableLanguages() const
static std::shared_ptr< ScaleDefinition > LoadFromFile(const std::string &jsonPath)
bool SaveToFile(const std::string &jsonPath)
ScaleQuestion * GetQuestion(const std::string &questionID)
static std::shared_ptr< ScaleDefinition > CreateNew(const std::string &code)
ValidationResult ValidateInternal() const
void AddTranslation(const std::string &language, const std::string &key, const std::string &value)
bool ExportToJSON(const std::string &definitionsPath, const std::string &translationsPath)
void AddQuestion(const ScaleQuestion &question)
static std::shared_ptr< ScaleDefinition > LoadFromOSDFile(const std::string &osdPath)
void MoveQuestion(int fromIndex, int toIndex)
bool ExportToOSD(const std::string &outputDir)
void RemoveQuestion(const std::string &questionID)
std::vector< NormThreshold > norms
std::string pooled_columns
std::string individual_file
std::vector< std::string > scores
std::map< std::string, std::vector< std::string > > correct_answers
std::vector< TransformStep > transform
std::vector< std::string > items
std::map< std::string, int > item_coding
std::map< std::string, double > weights
std::vector< NormThreshold > norms
std::map< std::string, std::vector< double > > value_map
std::vector< std::string > labels
std::string question_head
bool HasAnyValidation() const
std::vector< std::string > include
std::vector< std::string > footer_refs
std::string template_type
std::vector< std::string > warnings
std::vector< std::string > errors
std::vector< VisibleWhenCondition > visible_when
std::string enabled_param
std::string visible_when_logic
std::string license_explanation
std::vector< std::string > options
std::string vas_orientation
std::string gate_terminate_message_key
std::vector< VisibleWhenCondition > visible_when_simple
std::vector< std::string > rows
std::vector< VasAnchor > vas_anchors
QuestionValidation validation
std::vector< std::string > options
std::string gate_required_value
std::vector< std::string > correct
std::vector< std::string > section_randomize_fixed
std::vector< std::string > columns
std::vector< std::string > likert_labels
std::string visible_when_logic
bool visible_when_is_complex
std::string question_head
std::string gate_operator