PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
imaging_document_source.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
14
15#include <algorithm>
16#include <chrono>
17#include <random>
18#include <sstream>
19#include <unordered_map>
20
22
23using namespace kcenon::pacs::core;
24using namespace kcenon::pacs::encoding;
25
26// =============================================================================
27// KOS-specific DICOM Tags
28// =============================================================================
29
30namespace kos_tags {
31
32constexpr dicom_tag value_type{0x0040, 0xA040};
33constexpr dicom_tag concept_name_code_sequence{0x0040, 0xA043};
34constexpr dicom_tag content_template_sequence{0x0040, 0xA504};
35constexpr dicom_tag template_identifier{0x0040, 0xDB00};
36constexpr dicom_tag mapping_resource{0x0008, 0x0105};
38constexpr dicom_tag referenced_series_sequence{0x0008, 0x1115};
39constexpr dicom_tag referenced_sop_sequence{0x0008, 0x1199};
40constexpr dicom_tag referenced_sop_class_uid{0x0008, 0x1150};
41constexpr dicom_tag referenced_sop_instance_uid{0x0008, 0x1155};
42constexpr dicom_tag code_value{0x0008, 0x0100};
43constexpr dicom_tag coding_scheme_designator{0x0008, 0x0102};
44constexpr dicom_tag code_meaning{0x0008, 0x0104};
45constexpr dicom_tag completion_flag{0x0040, 0xA491};
46constexpr dicom_tag verification_flag{0x0040, 0xA493};
47
48} // namespace kos_tags
49
50// =============================================================================
51// Helper: insert a sequence element into a dataset
52// =============================================================================
53
54namespace {
55
56void insert_sequence(dicom_dataset& ds, dicom_tag tag,
57 std::vector<dicom_dataset> items) {
58 dicom_element seq_elem(tag, vr_type::SQ);
59 seq_elem.sequence_items() = std::move(items);
60 ds.insert(std::move(seq_elem));
61}
62
63std::string current_datetime() {
64 auto now = std::chrono::system_clock::now();
65 auto time_t_now = std::chrono::system_clock::to_time_t(now);
66 std::tm tm_now{};
67#if defined(_WIN32)
68 gmtime_s(&tm_now, &time_t_now);
69#else
70 gmtime_r(&time_t_now, &tm_now);
71#endif
72 char buf[16];
73 std::strftime(buf, sizeof(buf), "%Y%m%d%H%M%S", &tm_now);
74 return buf;
75}
76
77std::string current_date() {
78 return current_datetime().substr(0, 8);
79}
80
81std::string current_time() {
82 return current_datetime().substr(8, 6);
83}
84
85} // namespace
86
87// =============================================================================
88// imaging_document_source Implementation
89// =============================================================================
90
94
96 const std::string& study_instance_uid,
97 const std::vector<kos_instance_reference>& references,
98 const std::optional<core::dicom_dataset>& patient_demographics) const {
99
100 kos_creation_result result;
101
102 if (references.empty()) {
103 result.error_message = "No instance references provided";
104 return result;
105 }
106
108
109 // --- SOP Common Module ---
110 kos.set_string(tags::sop_class_uid, vr_type::UI,
113 kos.set_string(tags::sop_instance_uid, vr_type::UI, result.kos_instance_uid);
114
115 // --- Patient Module (Type 2) ---
116 if (patient_demographics) {
117 auto copy_if_present = [&](dicom_tag tag, vr_type vr) {
118 if (patient_demographics->contains(tag)) {
119 kos.set_string(tag, vr, patient_demographics->get_string(tag));
120 } else {
121 kos.set_string(tag, vr, "");
122 }
123 };
124 copy_if_present(tags::patient_name, vr_type::PN);
125 copy_if_present(tags::patient_id, vr_type::LO);
126 copy_if_present(tags::patient_birth_date, vr_type::DA);
127 copy_if_present(tags::patient_sex, vr_type::CS);
128 } else {
129 kos.set_string(tags::patient_name, vr_type::PN, "");
130 kos.set_string(tags::patient_id, vr_type::LO, "");
131 kos.set_string(tags::patient_birth_date, vr_type::DA, "");
132 kos.set_string(tags::patient_sex, vr_type::CS, "");
133 }
134
135 // --- General Study Module ---
136 kos.set_string(tags::study_instance_uid, vr_type::UI, study_instance_uid);
137 kos.set_string(tags::study_date, vr_type::DA, current_date());
138 kos.set_string(tags::study_time, vr_type::TM, current_time());
139 kos.set_string(tags::referring_physician_name, vr_type::PN, "");
140 kos.set_string(tags::study_id, vr_type::SH, "");
141 kos.set_string(tags::accession_number, vr_type::SH, "");
142
143 // --- General Series Module ---
144 kos.set_string(tags::modality, vr_type::CS, "KO");
146 kos.set_string(tags::series_number, vr_type::IS, "1");
147
148 // --- General Equipment Module ---
149 kos.set_string(dicom_tag{0x0008, 0x0070}, vr_type::LO, ""); // Manufacturer
150
151 // --- SR Document General Module ---
152 kos.set_string(tags::instance_number, vr_type::IS, "1");
153 kos.set_string(tags::content_date, vr_type::DA, current_date());
154 kos.set_string(tags::content_time, vr_type::TM, current_time());
155 kos.set_string(kos_tags::completion_flag, vr_type::CS, "COMPLETE");
156 kos.set_string(kos_tags::verification_flag, vr_type::CS, "UNVERIFIED");
157
158 // --- SR Document Content Module ---
159 // Value type: CONTAINER (root)
160 kos.set_string(kos_tags::value_type, vr_type::CS, "CONTAINER");
161
162 // Concept Name Code Sequence: Key Object Selection (113000, DCM)
163 dicom_dataset concept_name;
164 concept_name.set_string(kos_tags::code_value, vr_type::SH, "113000");
165 concept_name.set_string(kos_tags::coding_scheme_designator, vr_type::SH, "DCM");
166 concept_name.set_string(kos_tags::code_meaning, vr_type::LO, "Of Interest");
167 insert_sequence(kos, kos_tags::concept_name_code_sequence, {concept_name});
168
169 // Content Template Sequence: TID 2010
170 dicom_dataset template_item;
171 template_item.set_string(kos_tags::template_identifier, vr_type::CS, "2010");
172 template_item.set_string(kos_tags::mapping_resource, vr_type::CS, "DCMR");
173 insert_sequence(kos, kos_tags::content_template_sequence, {template_item});
174
175 // --- Current Requested Procedure Evidence Sequence ---
176 build_evidence_sequence(kos, references);
177
178 result.success = true;
179 result.reference_count = references.size();
180 result.kos_dataset = std::move(kos);
181
182 return result;
183}
184
186 const core::dicom_dataset& kos_dataset) const {
187
188 xds_document_entry entry;
189
190 entry.entry_uuid = "urn:uuid:" + generate_uid();
191 entry.unique_id = kos_dataset.get_string(tags::sop_instance_uid);
192 entry.patient_id = kos_dataset.get_string(tags::patient_id);
193 entry.source_patient_id = entry.patient_id;
194
195 entry.class_code = "IMG";
196 entry.class_code_scheme = "1.3.6.1.4.1.19376.3.840.1.1.4";
197 entry.class_code_display = "Imaging Procedure";
198
199 entry.type_code = "KOS";
200 entry.type_code_scheme = "1.2.840.10008.2.16.4";
201 entry.type_code_display = "Key Object Selection";
202
204 entry.format_code_scheme = "1.2.840.10008.2.6.1";
205
206 entry.creation_time = current_datetime();
207 entry.service_start_time = entry.creation_time;
208 entry.service_stop_time = entry.creation_time;
209
214
215 // Extract author from patient demographics if available
216 auto ref_physician = kos_dataset.get_string(tags::referring_physician_name);
217 if (!ref_physician.empty()) {
218 entry.author_person = ref_physician;
219 }
220
221 entry.title = "Key Object Selection Document";
222
223 return entry;
224}
225
227 const std::string& patient_id) const {
228
230
231 set.unique_id = generate_uid();
232 set.source_id = config_.source_oid;
233 set.patient_id = patient_id;
234 set.content_type_code = "IMG";
235 set.content_type_code_scheme = "1.3.6.1.4.1.19376.3.840.1.1.4";
236 set.content_type_code_display = "Imaging Procedure";
237 set.submission_time = current_datetime();
238
239 return set;
240}
241
243 [[maybe_unused]] const core::dicom_dataset& kos_dataset,
244 [[maybe_unused]] const xds_document_entry& entry) const {
245
246 publication_result result;
247
248 if (config_.registry_url.empty()) {
249 result.error_message = "XDS registry URL not configured";
250 return result;
251 }
252
253 // NOTE: Actual HTTP POST to the XDS registry/repository (ITI-41)
254 // would be implemented here using the network layer.
255 // The ITI-41 transaction uses SOAP/MTOM encoding.
256 // For now, return success to indicate the interface is operational.
257 result.success = true;
258 result.document_entry_uuid = entry.entry_uuid;
259
260 return result;
261}
262
265 return config_;
266}
267
272
274 static constexpr const char* uid_root = "1.2.826.0.1.3680043.2.1545.1";
275 static std::mt19937_64 gen{std::random_device{}()};
276 static std::uniform_int_distribution<uint64_t> dist;
277
278 auto now = std::chrono::system_clock::now();
279 auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(
280 now.time_since_epoch()).count();
281
282 return std::string(uid_root) + "." + std::to_string(timestamp) +
283 "." + std::to_string(dist(gen) % 100000);
284}
285
287 core::dicom_dataset& kos_dataset,
288 const std::vector<kos_instance_reference>& references) const {
289
290 // Group references by study -> series -> instances
291 struct series_group {
292 std::string series_uid;
293 std::vector<std::pair<std::string, std::string>> instances; // sop_class, sop_instance
294 };
295
296 std::unordered_map<std::string, std::vector<series_group>> study_map;
297
298 for (const auto& ref : references) {
299 auto& series_list = study_map[ref.study_instance_uid];
300
301 auto it = std::find_if(series_list.begin(), series_list.end(),
302 [&](const series_group& sg) {
303 return sg.series_uid == ref.series_instance_uid;
304 });
305
306 if (it != series_list.end()) {
307 it->instances.emplace_back(ref.sop_class_uid, ref.sop_instance_uid);
308 } else {
309 series_group sg;
310 sg.series_uid = ref.series_instance_uid;
311 sg.instances.emplace_back(ref.sop_class_uid, ref.sop_instance_uid);
312 series_list.push_back(std::move(sg));
313 }
314 }
315
316 // Build the sequence structure
317 std::vector<dicom_dataset> study_items;
318 for (const auto& [study_uid, series_list] : study_map) {
319 dicom_dataset study_item;
320 study_item.set_string(tags::study_instance_uid, vr_type::UI, study_uid);
321
322 std::vector<dicom_dataset> series_items;
323 for (const auto& sg : series_list) {
324 dicom_dataset series_item;
325 series_item.set_string(tags::series_instance_uid, vr_type::UI, sg.series_uid);
326
327 std::vector<dicom_dataset> sop_items;
328 for (const auto& [sop_class, sop_instance] : sg.instances) {
329 dicom_dataset sop_item;
330 sop_item.set_string(kos_tags::referenced_sop_class_uid, vr_type::UI, sop_class);
331 sop_item.set_string(kos_tags::referenced_sop_instance_uid, vr_type::UI, sop_instance);
332 sop_items.push_back(std::move(sop_item));
333 }
334 insert_sequence(series_item, kos_tags::referenced_sop_sequence, std::move(sop_items));
335 series_items.push_back(std::move(series_item));
336 }
337 insert_sequence(study_item, kos_tags::referenced_series_sequence, std::move(series_items));
338 study_items.push_back(std::move(study_item));
339 }
340
341 insert_sequence(kos_dataset,
343 std::move(study_items));
344}
345
346} // namespace kcenon::pacs::services::xds
void set_string(dicom_tag tag, encoding::vr_type vr, std::string_view value)
Set a string value for the given tag.
auto get_string(dicom_tag tag, std::string_view default_value="") const -> std::string
Get the string value of an element.
const imaging_document_source_config & config() const noexcept
Get current configuration.
std::string generate_uid() const
Generate a new UID for KOS instances.
void build_evidence_sequence(core::dicom_dataset &kos_dataset, const std::vector< kos_instance_reference > &references) const
Build the Current Requested Procedure Evidence Sequence.
publication_result publish_document(const core::dicom_dataset &kos_dataset, const xds_document_entry &entry) const
Publish a KOS document to the XDS registry/repository.
void set_config(const imaging_document_source_config &config)
Set configuration.
xds_submission_set build_submission_set(const std::string &patient_id) const
Build XDS submission set metadata.
kos_creation_result create_kos_document(const std::string &study_instance_uid, const std::vector< kos_instance_reference > &references, const std::optional< core::dicom_dataset > &patient_demographics=std::nullopt) const
Create a KOS document from a set of DICOM instance references.
xds_document_entry build_document_entry(const core::dicom_dataset &kos_dataset) const
Build XDS document entry metadata from a KOS dataset.
Compile-time constants for commonly used DICOM tags.
IHE XDS-I.b Imaging Document Source Actor.
constexpr dicom_tag referring_physician_name
Referring Physician's Name.
constexpr dicom_tag content_time
Content Time.
constexpr dicom_tag patient_id
Patient ID.
constexpr dicom_tag sop_instance_uid
SOP Instance UID.
constexpr dicom_tag patient_birth_date
Patient's Birth Date.
constexpr dicom_tag accession_number
Accession Number.
constexpr dicom_tag modality
Modality.
constexpr dicom_tag study_time
Study Time.
constexpr dicom_tag patient_sex
Patient's Sex.
constexpr dicom_tag study_instance_uid
Study Instance UID.
constexpr dicom_tag series_number
Series Number.
constexpr dicom_tag content_date
Content Date.
constexpr dicom_tag sop_class_uid
SOP Class UID.
constexpr dicom_tag study_id
Study ID.
constexpr dicom_tag patient_name
Patient's Name.
constexpr dicom_tag study_date
Study Date.
constexpr dicom_tag series_instance_uid
Series Instance UID.
constexpr dicom_tag instance_number
Instance Number.
vr_type
DICOM Value Representation (VR) types.
Definition vr_type.h:29
constexpr std::string_view key_object_selection_document_storage_uid
Key Object Selection Document Storage SOP Class UID.
Definition sr_storage.h:112
constexpr dicom_tag current_requested_procedure_evidence_sequence
Structured Report (SR) Storage SOP Classes.
Configuration for the Imaging Document Source actor.
std::string source_oid
Source system OID (used as sourceId in submissions)
std::string practice_setting_code
Default practice setting code.
std::string registry_url
XDS Registry/Repository endpoint URL.
Result of a KOS document creation operation.
std::optional< core::dicom_dataset > kos_dataset
The created KOS dataset (if successful)
size_t reference_count
Number of referenced instances.
std::string kos_instance_uid
SOP Instance UID of the created KOS.
bool success
Whether the KOS was created successfully.
Result of publishing a document to an XDS registry/repository.
std::string document_entry_uuid
Registry-assigned document entry UUID.
std::string error_message
Error message (if failed)
bool success
Whether the publication was successful.
Document entry metadata for XDS registry submission.
std::string creation_time
Creation time (DTM format: YYYYMMDDhhmmss)
std::string patient_id
Patient ID in CX format (ID^^^&OID&ISO)
std::string format_code_scheme
std::string type_code_display
std::string service_start_time
Service start/stop time.
std::string class_code
Document class code (e.g., "Imaging Procedure")
std::string class_code_scheme
std::string facility_type_code_scheme
std::string type_code
Type code (e.g., "Key Object Selection")
std::string unique_id
Document unique ID (OID format)
std::string facility_type_code
Healthcare facility type.
std::string source_patient_id
Source patient ID (from the originating system)
std::string type_code_scheme
std::string class_code_display
std::string author_person
Author information.
std::string format_code
Format code (e.g., "1.2.840.10008.5.1.4.1.1.88.59")
std::string entry_uuid
Unique identifier for this document entry.
std::string service_stop_time
std::string title
Title/description.
std::string practice_setting_code_scheme
std::string practice_setting_code
Practice setting code.
Submission set metadata for XDS registry.
vr_encoding vr