PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
result_formatter.h
Go to the documentation of this file.
1
11#ifndef QUERY_SCU_RESULT_FORMATTER_HPP
12#define QUERY_SCU_RESULT_FORMATTER_HPP
13
17
18#include <algorithm>
19#include <iomanip>
20#include <iostream>
21#include <sstream>
22#include <string>
23#include <vector>
24
25namespace query_scu {
26
30enum class output_format {
31 table,
32 json,
33 csv
34};
35
41[[nodiscard]] inline output_format parse_output_format(std::string_view format_str) {
42 if (format_str == "json") return output_format::json;
43 if (format_str == "csv") return output_format::csv;
45}
46
53public:
55
62 : format_(format), level_(level) {}
63
69 [[nodiscard]] std::string format(
70 const std::vector<kcenon::pacs::core::dicom_dataset>& results) const {
71 switch (format_) {
73 return format_json(results);
75 return format_csv(results);
77 default:
78 return format_table(results);
79 }
80 }
81
82private:
86 [[nodiscard]] std::string format_table(
87 const std::vector<kcenon::pacs::core::dicom_dataset>& results) const {
88 using namespace kcenon::pacs::core;
89 std::ostringstream oss;
90
91 if (results.empty()) {
92 oss << "No results found.\n";
93 return oss.str();
94 }
95
96 // Define columns based on query level
97 auto columns = get_columns_for_level();
98
99 // Calculate column widths
100 std::vector<size_t> widths;
101 widths.reserve(columns.size());
102 for (const auto& col : columns) {
103 widths.push_back(col.header.length());
104 }
105
106 // Update widths based on data
107 for (const auto& result : results) {
108 for (size_t i = 0; i < columns.size(); ++i) {
109 auto value = get_tag_value(result, columns[i].tag);
110 widths[i] = std::max(widths[i], value.length());
111 }
112 }
113
114 // Cap widths at reasonable maximum
115 for (auto& w : widths) {
116 w = std::min(w, size_t(40));
117 }
118
119 // Print header
120 oss << "\n=== Query Results (" << results.size() << " "
121 << kcenon::pacs::services::to_string(level_) << "(s)) ===\n\n";
122
123 // Print column headers
124 for (size_t i = 0; i < columns.size(); ++i) {
125 oss << std::left << std::setw(static_cast<int>(widths[i] + 2))
126 << columns[i].header;
127 }
128 oss << "\n";
129
130 // Print separator
131 for (size_t i = 0; i < columns.size(); ++i) {
132 oss << std::string(widths[i], '-') << " ";
133 }
134 oss << "\n";
135
136 // Print data rows
137 for (const auto& result : results) {
138 for (size_t i = 0; i < columns.size(); ++i) {
139 auto value = get_tag_value(result, columns[i].tag);
140 if (value.length() > widths[i]) {
141 value = value.substr(0, widths[i] - 3) + "...";
142 }
143 oss << std::left << std::setw(static_cast<int>(widths[i] + 2))
144 << value;
145 }
146 oss << "\n";
147 }
148
149 return oss.str();
150 }
151
155 [[nodiscard]] std::string format_json(
156 const std::vector<kcenon::pacs::core::dicom_dataset>& results) const {
157 using namespace kcenon::pacs::core;
158 std::ostringstream oss;
159
160 oss << "{\n";
161 oss << " \"queryLevel\": \"" << kcenon::pacs::services::to_string(level_) << "\",\n";
162 oss << " \"resultCount\": " << results.size() << ",\n";
163 oss << " \"results\": [\n";
164
165 auto columns = get_columns_for_level();
166
167 for (size_t i = 0; i < results.size(); ++i) {
168 const auto& result = results[i];
169 oss << " {\n";
170
171 for (size_t j = 0; j < columns.size(); ++j) {
172 auto value = get_tag_value(result, columns[j].tag);
173 oss << " \"" << columns[j].json_key << "\": \""
174 << escape_json(value) << "\"";
175 if (j < columns.size() - 1) {
176 oss << ",";
177 }
178 oss << "\n";
179 }
180
181 oss << " }";
182 if (i < results.size() - 1) {
183 oss << ",";
184 }
185 oss << "\n";
186 }
187
188 oss << " ]\n";
189 oss << "}\n";
190
191 return oss.str();
192 }
193
197 [[nodiscard]] std::string format_csv(
198 const std::vector<kcenon::pacs::core::dicom_dataset>& results) const {
199 using namespace kcenon::pacs::core;
200 std::ostringstream oss;
201
202 auto columns = get_columns_for_level();
203
204 // Print header row
205 for (size_t i = 0; i < columns.size(); ++i) {
206 oss << columns[i].header;
207 if (i < columns.size() - 1) {
208 oss << ",";
209 }
210 }
211 oss << "\n";
212
213 // Print data rows
214 for (const auto& result : results) {
215 for (size_t i = 0; i < columns.size(); ++i) {
216 auto value = get_tag_value(result, columns[i].tag);
217 oss << escape_csv(value);
218 if (i < columns.size() - 1) {
219 oss << ",";
220 }
221 }
222 oss << "\n";
223 }
224
225 return oss.str();
226 }
227
231 struct column_def {
232 std::string header;
234 std::string json_key;
235 };
236
240 [[nodiscard]] std::vector<column_def> get_columns_for_level() const {
241 using namespace kcenon::pacs::core;
242 std::vector<column_def> columns;
243
244 // Patient level columns (always included)
245 columns.push_back({"Patient Name", tags::patient_name, "patientName"});
246 columns.push_back({"Patient ID", tags::patient_id, "patientId"});
247
248 if (level_ == query_level::patient) {
249 columns.push_back({"Birth Date", tags::patient_birth_date, "birthDate"});
250 columns.push_back({"Sex", tags::patient_sex, "sex"});
251 return columns;
252 }
253
254 // Study level columns
255 columns.push_back({"Study Date", tags::study_date, "studyDate"});
256 columns.push_back({"Accession #", tags::accession_number, "accessionNumber"});
257 columns.push_back({"Description", tags::study_description, "studyDescription"});
258
259 if (level_ == query_level::study) {
260 columns.push_back({"Modalities", tags::modalities_in_study, "modalities"});
261 columns.push_back({"Study UID", tags::study_instance_uid, "studyInstanceUid"});
262 return columns;
263 }
264
265 // Series level columns
266 columns.push_back({"Modality", tags::modality, "modality"});
267 columns.push_back({"Series #", tags::series_number, "seriesNumber"});
268 columns.push_back({"Series Desc", tags::series_description, "seriesDescription"});
269
270 if (level_ == query_level::series) {
271 columns.push_back({"Series UID", tags::series_instance_uid, "seriesInstanceUid"});
272 return columns;
273 }
274
275 // Instance level columns
276 columns.push_back({"Instance #", tags::instance_number, "instanceNumber"});
277 columns.push_back({"SOP Class", tags::sop_class_uid, "sopClassUid"});
278 columns.push_back({"SOP Instance UID", tags::sop_instance_uid, "sopInstanceUid"});
279
280 return columns;
281 }
282
286 [[nodiscard]] static std::string get_tag_value(
289 return ds.get_string(tag);
290 }
291
295 [[nodiscard]] static std::string escape_json(const std::string& s) {
296 std::string result;
297 result.reserve(s.length());
298 for (char c : s) {
299 switch (c) {
300 case '"': result += "\\\""; break;
301 case '\\': result += "\\\\"; break;
302 case '\b': result += "\\b"; break;
303 case '\f': result += "\\f"; break;
304 case '\n': result += "\\n"; break;
305 case '\r': result += "\\r"; break;
306 case '\t': result += "\\t"; break;
307 default:
308 if (static_cast<unsigned char>(c) < 0x20) {
309 result += "\\u";
310 char buf[5];
311 std::snprintf(buf, sizeof(buf), "%04X",
312 static_cast<unsigned char>(c));
313 result += buf;
314 } else {
315 result += c;
316 }
317 }
318 }
319 return result;
320 }
321
325 [[nodiscard]] static std::string escape_csv(const std::string& s) {
326 if (s.find_first_of(",\"\n\r") == std::string::npos) {
327 return s;
328 }
329
330 std::string result = "\"";
331 for (char c : s) {
332 if (c == '"') {
333 result += "\"\"";
334 } else {
335 result += c;
336 }
337 }
338 result += "\"";
339 return result;
340 }
341
344};
345
346} // namespace query_scu
347
348#endif // QUERY_SCU_RESULT_FORMATTER_HPP
auto get_string(dicom_tag tag, std::string_view default_value="") const -> std::string
Get the string value of an element.
Result formatter for query results.
std::string format_csv(const std::vector< kcenon::pacs::core::dicom_dataset > &results) const
Format results as CSV.
static std::string get_tag_value(const kcenon::pacs::core::dicom_dataset &ds, kcenon::pacs::core::dicom_tag tag)
Get tag value from dataset as string.
std::string format_table(const std::vector< kcenon::pacs::core::dicom_dataset > &results) const
Format results as a human-readable table.
std::vector< column_def > get_columns_for_level() const
Get columns appropriate for the query level.
std::string format(const std::vector< kcenon::pacs::core::dicom_dataset > &results) const
Format query results.
std::string format_json(const std::vector< kcenon::pacs::core::dicom_dataset > &results) const
Format results as JSON.
static std::string escape_csv(const std::string &s)
Escape string for CSV output.
result_formatter(output_format format, query_level level)
Construct formatter with output format.
static std::string escape_json(const std::string &s)
Escape string for JSON output.
DICOM Dataset - ordered collection of Data Elements.
Compile-time constants for commonly used DICOM tags.
auto to_string(mpps_status status) -> std::string_view
Convert mpps_status to DICOM string representation.
Definition mpps_scp.h:60
query_level
DICOM Query/Retrieve level enumeration.
Definition query_scp.h:63
output_format
Output format enumeration.
@ json
JSON format for integration.
@ csv
CSV format for export.
@ table
Human-readable table format.
output_format parse_output_format(std::string_view format_str)
Parse output format from string.
DICOM Query SCP service (C-FIND handler)
Column definition for formatting.
kcenon::pacs::core::dicom_tag tag