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
15
16#include <algorithm>
17#include <chrono>
18#include <random>
19#include <sstream>
20#include <unordered_map>
21
23
24using namespace kcenon::pacs::core;
25using namespace kcenon::pacs::encoding;
26
27// =============================================================================
28// KOS-specific DICOM Tags
29// =============================================================================
30
31namespace kos_tags {
32
33constexpr dicom_tag value_type{0x0040, 0xA040};
34constexpr dicom_tag concept_name_code_sequence{0x0040, 0xA043};
35constexpr dicom_tag content_template_sequence{0x0040, 0xA504};
36constexpr dicom_tag template_identifier{0x0040, 0xDB00};
37constexpr dicom_tag mapping_resource{0x0008, 0x0105};
39constexpr dicom_tag referenced_series_sequence{0x0008, 0x1115};
40constexpr dicom_tag referenced_sop_sequence{0x0008, 0x1199};
41constexpr dicom_tag referenced_sop_class_uid{0x0008, 0x1150};
42constexpr dicom_tag referenced_sop_instance_uid{0x0008, 0x1155};
43constexpr dicom_tag code_value{0x0008, 0x0100};
44constexpr dicom_tag coding_scheme_designator{0x0008, 0x0102};
45constexpr dicom_tag code_meaning{0x0008, 0x0104};
46constexpr dicom_tag completion_flag{0x0040, 0xA491};
47constexpr dicom_tag verification_flag{0x0040, 0xA493};
48
49} // namespace kos_tags
50
51// =============================================================================
52// Helper: insert a sequence element into a dataset
53// =============================================================================
54
55namespace {
56
57void insert_sequence(dicom_dataset& ds, dicom_tag tag,
58 std::vector<dicom_dataset> items) {
59 dicom_element seq_elem(tag, vr_type::SQ);
60 seq_elem.sequence_items() = std::move(items);
61 ds.insert(std::move(seq_elem));
62}
63
64std::string current_datetime() {
65 auto now = std::chrono::system_clock::now();
66 auto time_t_now = std::chrono::system_clock::to_time_t(now);
67 std::tm tm_now{};
68#if defined(_WIN32)
69 gmtime_s(&tm_now, &time_t_now);
70#else
71 gmtime_r(&time_t_now, &tm_now);
72#endif
73 char buf[16];
74 std::strftime(buf, sizeof(buf), "%Y%m%d%H%M%S", &tm_now);
75 return buf;
76}
77
78std::string current_date() {
79 return current_datetime().substr(0, 8);
80}
81
82std::string current_time() {
83 return current_datetime().substr(8, 6);
84}
85
86} // namespace
87
88// =============================================================================
89// imaging_document_source Implementation
90// =============================================================================
91
95
97 const std::string& study_instance_uid,
98 const std::vector<kos_instance_reference>& references,
99 const std::optional<core::dicom_dataset>& patient_demographics) const {
100
101 kos_creation_result result;
102
103 if (references.empty()) {
104 result.error_message = "No instance references provided";
105 return result;
106 }
107
109
110 // --- SOP Common Module ---
111 kos.set_string(tags::sop_class_uid, vr_type::UI,
114 kos.set_string(tags::sop_instance_uid, vr_type::UI, result.kos_instance_uid);
115
116 // --- Patient Module (Type 2) ---
117 if (patient_demographics) {
118 auto copy_if_present = [&](dicom_tag tag, vr_type vr) {
119 if (patient_demographics->contains(tag)) {
120 kos.set_string(tag, vr, patient_demographics->get_string(tag));
121 } else {
122 kos.set_string(tag, vr, "");
123 }
124 };
125 copy_if_present(tags::patient_name, vr_type::PN);
126 copy_if_present(tags::patient_id, vr_type::LO);
127 copy_if_present(tags::patient_birth_date, vr_type::DA);
128 copy_if_present(tags::patient_sex, vr_type::CS);
129 } else {
130 kos.set_string(tags::patient_name, vr_type::PN, "");
131 kos.set_string(tags::patient_id, vr_type::LO, "");
132 kos.set_string(tags::patient_birth_date, vr_type::DA, "");
133 kos.set_string(tags::patient_sex, vr_type::CS, "");
134 }
135
136 // --- General Study Module ---
137 kos.set_string(tags::study_instance_uid, vr_type::UI, study_instance_uid);
138 kos.set_string(tags::study_date, vr_type::DA, current_date());
139 kos.set_string(tags::study_time, vr_type::TM, current_time());
140 kos.set_string(tags::referring_physician_name, vr_type::PN, "");
141 kos.set_string(tags::study_id, vr_type::SH, "");
142 kos.set_string(tags::accession_number, vr_type::SH, "");
143
144 // --- General Series Module ---
145 kos.set_string(tags::modality, vr_type::CS, "KO");
147 kos.set_string(tags::series_number, vr_type::IS, "1");
148
149 // --- General Equipment Module ---
150 kos.set_string(dicom_tag{0x0008, 0x0070}, vr_type::LO, ""); // Manufacturer
151
152 // --- SR Document General Module ---
153 kos.set_string(tags::instance_number, vr_type::IS, "1");
154 kos.set_string(tags::content_date, vr_type::DA, current_date());
155 kos.set_string(tags::content_time, vr_type::TM, current_time());
156 kos.set_string(kos_tags::completion_flag, vr_type::CS, "COMPLETE");
157 kos.set_string(kos_tags::verification_flag, vr_type::CS, "UNVERIFIED");
158
159 // --- SR Document Content Module ---
160 // Value type: CONTAINER (root)
161 kos.set_string(kos_tags::value_type, vr_type::CS, "CONTAINER");
162
163 // Concept Name Code Sequence: Key Object Selection (113000, DCM)
164 dicom_dataset concept_name;
165 concept_name.set_string(kos_tags::code_value, vr_type::SH, "113000");
166 concept_name.set_string(kos_tags::coding_scheme_designator, vr_type::SH, "DCM");
167 concept_name.set_string(kos_tags::code_meaning, vr_type::LO, "Of Interest");
168 insert_sequence(kos, kos_tags::concept_name_code_sequence, {concept_name});
169
170 // Content Template Sequence: TID 2010
171 dicom_dataset template_item;
172 template_item.set_string(kos_tags::template_identifier, vr_type::CS, "2010");
173 template_item.set_string(kos_tags::mapping_resource, vr_type::CS, "DCMR");
174 insert_sequence(kos, kos_tags::content_template_sequence, {template_item});
175
176 // --- Current Requested Procedure Evidence Sequence ---
177 build_evidence_sequence(kos, references);
178
179 result.success = true;
180 result.reference_count = references.size();
181 result.kos_dataset = std::move(kos);
182
183 return result;
184}
185
187 const core::dicom_dataset& kos_dataset) const {
188
189 xds_document_entry entry;
190
191 entry.entry_uuid = "urn:uuid:" + generate_uid();
192 entry.unique_id = kos_dataset.get_string(tags::sop_instance_uid);
193 entry.patient_id = kos_dataset.get_string(tags::patient_id);
194 entry.source_patient_id = entry.patient_id;
195
196 entry.class_code = "IMG";
197 entry.class_code_scheme = "1.3.6.1.4.1.19376.3.840.1.1.4";
198 entry.class_code_display = "Imaging Procedure";
199
200 entry.type_code = "KOS";
201 entry.type_code_scheme = "1.2.840.10008.2.16.4";
202 entry.type_code_display = "Key Object Selection";
203
205 entry.format_code_scheme = "1.2.840.10008.2.6.1";
206
207 entry.creation_time = current_datetime();
208 entry.service_start_time = entry.creation_time;
209 entry.service_stop_time = entry.creation_time;
210
215
216 // Extract author from patient demographics if available
217 auto ref_physician = kos_dataset.get_string(tags::referring_physician_name);
218 if (!ref_physician.empty()) {
219 entry.author_person = ref_physician;
220 }
221
222 entry.title = "Key Object Selection Document";
223
224 return entry;
225}
226
228 const std::string& patient_id) const {
229
231
232 set.unique_id = generate_uid();
234 set.patient_id = patient_id;
235 set.content_type_code = "IMG";
236 set.content_type_code_scheme = "1.3.6.1.4.1.19376.3.840.1.1.4";
237 set.content_type_code_display = "Imaging Procedure";
238 set.submission_time = current_datetime();
239
240 return set;
241}
242
243namespace {
244
246std::vector<std::string> collect_sop_instance_uids(
247 const core::dicom_dataset& kos_dataset) {
248 std::vector<std::string> uids;
249 const auto* evidence_elem =
251 if (!evidence_elem || !evidence_elem->is_sequence()) {
252 return uids;
253 }
254 for (const auto& study_item : evidence_elem->sequence_items()) {
255 const auto* series_elem =
257 if (!series_elem || !series_elem->is_sequence()) continue;
258 for (const auto& series_item : series_elem->sequence_items()) {
259 const auto* sop_elem =
260 series_item.get(kos_tags::referenced_sop_sequence);
261 if (!sop_elem || !sop_elem->is_sequence()) continue;
262 for (const auto& sop_item : sop_elem->sequence_items()) {
263 auto uid = sop_item.get_string(kos_tags::referenced_sop_instance_uid);
264 if (!uid.empty()) uids.push_back(std::move(uid));
265 }
266 }
267 }
268 return uids;
269}
270
271} // namespace
272
274 const core::dicom_dataset& kos_dataset,
275 const xds_document_entry& entry,
276 bool is_imaging) const {
277
278 publication_result result;
279
280 // Build audit fields once; used for both success and failure paths.
281 auto study_uid = kos_dataset.get_string(core::tags::study_instance_uid);
282 auto patient_id = kos_dataset.get_string(core::tags::patient_id);
283 auto sop_instance_uids = collect_sop_instance_uids(kos_dataset);
284 const auto transaction =
285 is_imaging
288
289 if (config_.registry_url.empty()) {
290 result.error_message = "XDS registry URL not configured";
291 if (auditor_) {
293 transaction,
295 config_.registry_url, // empty when unconfigured
296 study_uid,
297 patient_id,
298 sop_instance_uids,
299 false);
300 }
301 return result;
302 }
303
304 // NOTE: Actual HTTP POST to the XDS registry/repository (ITI-41 / RAD-68)
305 // would be implemented here using the network layer.
306 // The ITI-41 / RAD-68 transactions use SOAP/MTOM encoding.
307 // For now, return success to indicate the interface is operational.
308 result.success = true;
309 result.document_entry_uuid = entry.entry_uuid;
310
311 if (auditor_) {
313 transaction,
316 study_uid,
317 patient_id,
318 sop_instance_uids,
319 true);
320 }
321
322 return result;
323}
324
326 std::shared_ptr<kcenon::pacs::security::atna_service_auditor> auditor) {
327 auditor_ = std::move(auditor);
328}
329
332 return config_;
333}
334
339
341 static constexpr const char* uid_root = "1.2.826.0.1.3680043.2.1545.1";
342 static std::mt19937_64 gen{std::random_device{}()};
343 static std::uniform_int_distribution<uint64_t> dist;
344
345 auto now = std::chrono::system_clock::now();
346 auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(
347 now.time_since_epoch()).count();
348
349 return std::string(uid_root) + "." + std::to_string(timestamp) +
350 "." + std::to_string(dist(gen) % 100000);
351}
352
354 core::dicom_dataset& kos_dataset,
355 const std::vector<kos_instance_reference>& references) const {
356
357 // Group references by study -> series -> instances
358 struct series_group {
359 std::string series_uid;
360 std::vector<std::pair<std::string, std::string>> instances; // sop_class, sop_instance
361 };
362
363 std::unordered_map<std::string, std::vector<series_group>> study_map;
364
365 for (const auto& ref : references) {
366 auto& series_list = study_map[ref.study_instance_uid];
367
368 auto it = std::find_if(series_list.begin(), series_list.end(),
369 [&](const series_group& sg) {
370 return sg.series_uid == ref.series_instance_uid;
371 });
372
373 if (it != series_list.end()) {
374 it->instances.emplace_back(ref.sop_class_uid, ref.sop_instance_uid);
375 } else {
376 series_group sg;
377 sg.series_uid = ref.series_instance_uid;
378 sg.instances.emplace_back(ref.sop_class_uid, ref.sop_instance_uid);
379 series_list.push_back(std::move(sg));
380 }
381 }
382
383 // Build the sequence structure
384 std::vector<dicom_dataset> study_items;
385 for (const auto& [study_uid, series_list] : study_map) {
386 dicom_dataset study_item;
387 study_item.set_string(tags::study_instance_uid, vr_type::UI, study_uid);
388
389 std::vector<dicom_dataset> series_items;
390 for (const auto& sg : series_list) {
391 dicom_dataset series_item;
392 series_item.set_string(tags::series_instance_uid, vr_type::UI, sg.series_uid);
393
394 std::vector<dicom_dataset> sop_items;
395 for (const auto& [sop_class, sop_instance] : sg.instances) {
396 dicom_dataset sop_item;
397 sop_item.set_string(kos_tags::referenced_sop_class_uid, vr_type::UI, sop_class);
398 sop_item.set_string(kos_tags::referenced_sop_instance_uid, vr_type::UI, sop_instance);
399 sop_items.push_back(std::move(sop_item));
400 }
401 insert_sequence(series_item, kos_tags::referenced_sop_sequence, std::move(sop_items));
402 series_items.push_back(std::move(series_item));
403 }
404 insert_sequence(study_item, kos_tags::referenced_series_sequence, std::move(series_items));
405 study_items.push_back(std::move(study_item));
406 }
407
408 insert_sequence(kos_dataset,
410 std::move(study_items));
411}
412
413} // namespace kcenon::pacs::services::xds
High-level facade for ATNA audit logging in DICOM services.
Represents a DICOM Dataset (ordered collection of Data Elements)
auto get(dicom_tag tag) noexcept -> dicom_element *
Get a pointer to the element with the given tag.
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.
Represents a DICOM tag (Group, Element pair)
Definition dicom_tag.h:51
@ rad_68
RAD-68 Provide and Register Imaging Document Set.
@ iti_41
ITI-41 Provide and Register Document Set-b.
virtual void audit_xds_provide_and_register(xds_source_transaction transaction, const std::string &source_ae, const std::string &dest_ae, const std::string &study_uid, const std::string &patient_id, const std::vector< std::string > &sop_instance_uids, bool success)
Audit an XDS-I.b Provide and Register Document Set transaction.
const imaging_document_source_config & config() const noexcept
Get current configuration.
publication_result publish_document(const core::dicom_dataset &kos_dataset, const xds_document_entry &entry, bool is_imaging=true) const
Publish a KOS document to the XDS registry/repository.
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.
std::shared_ptr< kcenon::pacs::security::atna_service_auditor > auditor_
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.
void set_audit_handler(std::shared_ptr< kcenon::pacs::security::atna_service_auditor > auditor)
Install an ATNA audit handler for XDS-I.b transactions.
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.
std::string unique_id
Unique identifier for this submission set.
std::string source_id
Source ID (OID of the submitting system)
std::string submission_time
Submission time (DTM format)
vr_encoding vr
std::string_view uid