PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
annotation_repository.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
16
17#include <chrono>
18#include <iomanip>
19#include <sstream>
20
21#ifdef PACS_WITH_DATABASE_SYSTEM
22
23namespace kcenon::pacs::storage {
24
25// =============================================================================
26// Constructor
27// =============================================================================
28
30 std::shared_ptr<pacs_database_adapter> db)
31 : base_repository(std::move(db), "annotations", "annotation_id") {}
32
33// =============================================================================
34// JSON Serialization
35// =============================================================================
36
37std::string annotation_repository::serialize_style(const annotation_style& style) {
38 std::ostringstream oss;
39 oss << "{"
40 << R"("color":")" << style.color << "\","
41 << R"("line_width":)" << style.line_width << ","
42 << R"("fill_color":")" << style.fill_color << "\","
43 << R"("fill_opacity":)" << style.fill_opacity << ","
44 << R"("font_family":")" << style.font_family << "\","
45 << R"("font_size":)" << style.font_size
46 << "}";
47 return oss.str();
48}
49
50annotation_style annotation_repository::deserialize_style(std::string_view json) {
51 annotation_style style;
52 if (json.empty() || json == "{}") return style;
53
54 auto find_string_value = [&](const char* key) -> std::string {
55 std::string search = std::string("\"") + key + "\":\"";
56 auto pos = json.find(search);
57 if (pos == std::string_view::npos) return "";
58 pos += search.size();
59 auto end = json.find('"', pos);
60 if (end == std::string_view::npos) return "";
61 return std::string(json.substr(pos, end - pos));
62 };
63
64 auto find_int_value = [&](const char* key) -> int {
65 std::string search = std::string("\"") + key + "\":";
66 auto pos = json.find(search);
67 if (pos == std::string_view::npos) return 0;
68 pos += search.size();
69 return std::atoi(json.data() + pos);
70 };
71
72 auto find_float_value = [&](const char* key) -> float {
73 std::string search = std::string("\"") + key + "\":";
74 auto pos = json.find(search);
75 if (pos == std::string_view::npos) return 0.0f;
76 pos += search.size();
77 return static_cast<float>(std::atof(json.data() + pos));
78 };
79
80 auto color = find_string_value("color");
81 if (!color.empty()) style.color = color;
82
83 style.line_width = find_int_value("line_width");
84 if (style.line_width == 0) style.line_width = 2;
85
86 style.fill_color = find_string_value("fill_color");
87 style.fill_opacity = find_float_value("fill_opacity");
88
89 auto font_family = find_string_value("font_family");
90 if (!font_family.empty()) style.font_family = font_family;
91
92 style.font_size = find_int_value("font_size");
93 if (style.font_size == 0) style.font_size = 14;
94
95 return style;
96}
97
98// =============================================================================
99// Timestamp Helpers
100// =============================================================================
101
102auto annotation_repository::parse_timestamp(const std::string& str) const
103 -> std::chrono::system_clock::time_point {
104 if (str.empty()) {
105 return {};
106 }
107
108 std::tm tm{};
109 std::istringstream ss(str);
110 ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S");
111 if (ss.fail()) {
112 return {};
113 }
114
115#ifdef _WIN32
116 auto time = _mkgmtime(&tm);
117#else
118 auto time = timegm(&tm);
119#endif
120
121 return std::chrono::system_clock::from_time_t(time);
122}
123
124auto annotation_repository::format_timestamp(
125 std::chrono::system_clock::time_point tp) const -> std::string {
126 if (tp == std::chrono::system_clock::time_point{}) {
127 return "";
128 }
129
130 auto time = std::chrono::system_clock::to_time_t(tp);
131 std::tm tm{};
132#ifdef _WIN32
133 gmtime_s(&tm, &time);
134#else
135 gmtime_r(&time, &tm);
136#endif
137
138 char buf[32];
139 std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm);
140 return buf;
141}
142
143// =============================================================================
144// Domain-Specific Operations
145// =============================================================================
146
147auto annotation_repository::find_by_pk(int64_t pk) -> result_type {
148 if (!db() || !db()->is_connected()) {
149 return result_type(kcenon::common::error_info{
150 -1, "Database not connected", "storage"});
151 }
152
153 auto builder = query_builder();
154 builder.select(select_columns())
155 .from(table_name())
156 .where("pk", "=", pk)
157 .limit(1);
158
159 auto result = storage_session().select(builder.build());
160 if (result.is_err()) {
161 return result_type(result.error());
162 }
163
164 if (result.value().empty()) {
165 return result_type(kcenon::common::error_info{
166 -1, "Annotation not found with pk=" + std::to_string(pk), "storage"});
167 }
168
169 return result_type(map_row_to_entity(result.value()[0]));
170}
171
172auto annotation_repository::find_by_instance(std::string_view sop_instance_uid)
173 -> list_result_type {
174 return find_where("sop_instance_uid", "=", std::string(sop_instance_uid));
175}
176
177auto annotation_repository::find_by_study(std::string_view study_uid)
178 -> list_result_type {
179 return find_where("study_uid", "=", std::string(study_uid));
180}
181
182auto annotation_repository::search(const annotation_query& query)
183 -> list_result_type {
184 if (!db() || !db()->is_connected()) {
185 return list_result_type(kcenon::common::error_info{
186 -1, "Database not connected", "storage"});
187 }
188
189 auto builder = query_builder();
190 builder.select(select_columns()).from(table_name());
191
192 std::optional<database::query_condition> condition;
193
194 if (query.study_uid.has_value()) {
195 auto cond = database::query_condition(
196 "study_uid", "=", query.study_uid.value());
197 condition = cond;
198 }
199
200 if (query.series_uid.has_value()) {
201 auto cond = database::query_condition(
202 "series_uid", "=", query.series_uid.value());
203 if (condition.has_value()) {
204 condition = condition.value() && cond;
205 } else {
206 condition = cond;
207 }
208 }
209
210 if (query.sop_instance_uid.has_value()) {
211 auto cond = database::query_condition(
212 "sop_instance_uid", "=", query.sop_instance_uid.value());
213 if (condition.has_value()) {
214 condition = condition.value() && cond;
215 } else {
216 condition = cond;
217 }
218 }
219
220 if (query.user_id.has_value()) {
221 auto cond = database::query_condition(
222 "user_id", "=", query.user_id.value());
223 if (condition.has_value()) {
224 condition = condition.value() && cond;
225 } else {
226 condition = cond;
227 }
228 }
229
230 if (query.type.has_value()) {
231 auto cond = database::query_condition(
232 "annotation_type", "=", to_string(query.type.value()));
233 if (condition.has_value()) {
234 condition = condition.value() && cond;
235 } else {
236 condition = cond;
237 }
238 }
239
240 if (condition.has_value()) {
241 builder.where(condition.value());
242 }
243
244 builder.order_by("created_at", database::sort_order::desc);
245
246 if (query.limit > 0) {
247 builder.limit(query.limit);
248 if (query.offset > 0) {
249 builder.offset(query.offset);
250 }
251 }
252
253 auto result = storage_session().select(builder.build());
254 if (result.is_err()) {
255 return list_result_type(result.error());
256 }
257
258 std::vector<annotation_record> records;
259 records.reserve(result.value().size());
260 for (const auto& row : result.value()) {
261 records.push_back(map_row_to_entity(row));
262 }
263
264 return list_result_type(std::move(records));
265}
266
267auto annotation_repository::update_annotation(const annotation_record& record)
268 -> VoidResult {
269 if (!db() || !db()->is_connected()) {
270 return VoidResult(kcenon::common::error_info{
271 -1, "Database not connected", "storage"});
272 }
273
274 auto now_str = format_timestamp(std::chrono::system_clock::now());
275 auto style_json = serialize_style(record.style);
276
277 auto builder = query_builder();
278 builder.update(table_name())
279 .set("geometry_json", record.geometry_json)
280 .set("text", record.text)
281 .set("style_json", style_json)
282 .set("updated_at", now_str)
283 .where("annotation_id", "=", record.annotation_id);
284
285 auto result = storage_session().execute(builder.build());
286 if (result.is_err()) {
287 return VoidResult(result.error());
288 }
289
290 return kcenon::common::ok();
291}
292
293auto annotation_repository::count_matching(const annotation_query& query)
294 -> Result<size_t> {
295 if (!db() || !db()->is_connected()) {
296 return Result<size_t>(kcenon::common::error_info{
297 -1, "Database not connected", "storage"});
298 }
299
300 auto builder = query_builder();
301 builder.select({"pk"}).from(table_name());
302
303 std::optional<database::query_condition> condition;
304
305 if (query.study_uid.has_value()) {
306 auto cond = database::query_condition(
307 "study_uid", "=", query.study_uid.value());
308 condition = cond;
309 }
310
311 if (query.series_uid.has_value()) {
312 auto cond = database::query_condition(
313 "series_uid", "=", query.series_uid.value());
314 if (condition.has_value()) {
315 condition = condition.value() && cond;
316 } else {
317 condition = cond;
318 }
319 }
320
321 if (query.sop_instance_uid.has_value()) {
322 auto cond = database::query_condition(
323 "sop_instance_uid", "=", query.sop_instance_uid.value());
324 if (condition.has_value()) {
325 condition = condition.value() && cond;
326 } else {
327 condition = cond;
328 }
329 }
330
331 if (query.user_id.has_value()) {
332 auto cond = database::query_condition(
333 "user_id", "=", query.user_id.value());
334 if (condition.has_value()) {
335 condition = condition.value() && cond;
336 } else {
337 condition = cond;
338 }
339 }
340
341 if (query.type.has_value()) {
342 auto cond = database::query_condition(
343 "annotation_type", "=", to_string(query.type.value()));
344 if (condition.has_value()) {
345 condition = condition.value() && cond;
346 } else {
347 condition = cond;
348 }
349 }
350
351 if (condition.has_value()) {
352 builder.where(condition.value());
353 }
354
355 auto result = storage_session().select(builder.build());
356 if (result.is_err()) {
357 return Result<size_t>(result.error());
358 }
359
360 return Result<size_t>(result.value().size());
361}
362
363// =============================================================================
364// base_repository Overrides
365// =============================================================================
366
367auto annotation_repository::map_row_to_entity(const database_row& row) const
368 -> annotation_record {
369 annotation_record record;
370
371 record.pk = std::stoll(row.at("pk"));
372 record.annotation_id = row.at("annotation_id");
373 record.study_uid = row.at("study_uid");
374 record.series_uid = row.at("series_uid");
375 record.sop_instance_uid = row.at("sop_instance_uid");
376
377 auto frame_it = row.find("frame_number");
378 if (frame_it != row.end() && !frame_it->second.empty()) {
379 record.frame_number = std::stoi(frame_it->second);
380 }
381
382 record.user_id = row.at("user_id");
383
384 auto type_str = row.at("annotation_type");
385 record.type = annotation_type_from_string(type_str).value_or(annotation_type::text);
386
387 record.geometry_json = row.at("geometry_json");
388 record.text = row.at("text");
389
390 auto style_it = row.find("style_json");
391 if (style_it != row.end() && !style_it->second.empty()) {
392 record.style = deserialize_style(style_it->second);
393 }
394
395 auto created_it = row.find("created_at");
396 if (created_it != row.end() && !created_it->second.empty()) {
397 record.created_at = parse_timestamp(created_it->second);
398 }
399
400 auto updated_it = row.find("updated_at");
401 if (updated_it != row.end() && !updated_it->second.empty()) {
402 record.updated_at = parse_timestamp(updated_it->second);
403 }
404
405 return record;
406}
407
408auto annotation_repository::entity_to_row(const annotation_record& entity) const
409 -> std::map<std::string, database_value> {
410 std::map<std::string, database_value> row;
411
412 row["annotation_id"] = entity.annotation_id;
413 row["study_uid"] = entity.study_uid;
414 row["series_uid"] = entity.series_uid;
415 row["sop_instance_uid"] = entity.sop_instance_uid;
416
417 if (entity.frame_number.has_value()) {
418 row["frame_number"] = static_cast<int64_t>(entity.frame_number.value());
419 } else {
420 row["frame_number"] = static_cast<int64_t>(0);
421 }
422
423 row["user_id"] = entity.user_id;
424 row["annotation_type"] = to_string(entity.type);
425 row["geometry_json"] = entity.geometry_json;
426 row["text"] = entity.text;
427 row["style_json"] = serialize_style(entity.style);
428
429 auto now = std::chrono::system_clock::now();
430 if (entity.created_at != std::chrono::system_clock::time_point{}) {
431 row["created_at"] = format_timestamp(entity.created_at);
432 } else {
433 row["created_at"] = format_timestamp(now);
434 }
435
436 if (entity.updated_at != std::chrono::system_clock::time_point{}) {
437 row["updated_at"] = format_timestamp(entity.updated_at);
438 } else {
439 row["updated_at"] = format_timestamp(now);
440 }
441
442 return row;
443}
444
445auto annotation_repository::get_pk(const annotation_record& entity) const
446 -> std::string {
447 return entity.annotation_id;
448}
449
450auto annotation_repository::has_pk(const annotation_record& entity) const
451 -> bool {
452 return !entity.annotation_id.empty();
453}
454
455auto annotation_repository::select_columns() const -> std::vector<std::string> {
456 return {"pk", "annotation_id", "study_uid", "series_uid",
457 "sop_instance_uid", "frame_number", "user_id",
458 "annotation_type", "geometry_json", "text",
459 "style_json", "created_at", "updated_at"};
460}
461
462} // namespace kcenon::pacs::storage
463
464#else // !PACS_WITH_DATABASE_SYSTEM
465
466// =============================================================================
467// Legacy SQLite Implementation
468// =============================================================================
469
470#include <sqlite3.h>
471
472namespace kcenon::pacs::storage {
473
474namespace {
475
476[[nodiscard]] std::string to_timestamp_string(
477 std::chrono::system_clock::time_point tp) {
478 if (tp == std::chrono::system_clock::time_point{}) {
479 return "";
480 }
481 auto time = std::chrono::system_clock::to_time_t(tp);
482 std::tm tm{};
483#ifdef _WIN32
484 gmtime_s(&tm, &time);
485#else
486 gmtime_r(&time, &tm);
487#endif
488 char buf[32];
489 std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm);
490 return buf;
491}
492
493[[nodiscard]] std::chrono::system_clock::time_point from_timestamp_string(
494 const char* str) {
495 if (!str || str[0] == '\0') {
496 return {};
497 }
498 std::tm tm{};
499 std::istringstream ss(str);
500 ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S");
501 if (ss.fail()) {
502 return {};
503 }
504#ifdef _WIN32
505 auto time = _mkgmtime(&tm);
506#else
507 auto time = timegm(&tm);
508#endif
509 return std::chrono::system_clock::from_time_t(time);
510}
511
512[[nodiscard]] std::string get_text_column(sqlite3_stmt* stmt, int col) {
513 auto text = reinterpret_cast<const char*>(sqlite3_column_text(stmt, col));
514 return text ? text : "";
515}
516
517[[nodiscard]] [[maybe_unused]] int get_int_column(sqlite3_stmt* stmt, int col, int default_val = 0) {
518 if (sqlite3_column_type(stmt, col) == SQLITE_NULL) {
519 return default_val;
520 }
521 return sqlite3_column_int(stmt, col);
522}
523
524[[nodiscard]] int64_t get_int64_column(sqlite3_stmt* stmt, int col, int64_t default_val = 0) {
525 if (sqlite3_column_type(stmt, col) == SQLITE_NULL) {
526 return default_val;
527 }
528 return sqlite3_column_int64(stmt, col);
529}
530
531[[nodiscard]] std::optional<int> get_optional_int(sqlite3_stmt* stmt, int col) {
532 if (sqlite3_column_type(stmt, col) == SQLITE_NULL) {
533 return std::nullopt;
534 }
535 return sqlite3_column_int(stmt, col);
536}
537
538void bind_optional_int(sqlite3_stmt* stmt, int idx, const std::optional<int>& value) {
539 if (value.has_value()) {
540 sqlite3_bind_int(stmt, idx, value.value());
541 } else {
542 sqlite3_bind_null(stmt, idx);
543 }
544}
545
546[[nodiscard]] std::string json_escape(std::string_view s) {
547 std::string result;
548 result.reserve(s.size());
549 for (char c : s) {
550 switch (c) {
551 case '"': result += "\\\""; break;
552 case '\\': result += "\\\\"; break;
553 case '\n': result += "\\n"; break;
554 case '\r': result += "\\r"; break;
555 case '\t': result += "\\t"; break;
556 default: result += c;
557 }
558 }
559 return result;
560}
561
562} // namespace
563
565 std::ostringstream oss;
566 oss << "{"
567 << R"("color":")" << json_escape(style.color) << "\","
568 << R"("line_width":)" << style.line_width << ","
569 << R"("fill_color":")" << json_escape(style.fill_color) << "\","
570 << R"("fill_opacity":)" << style.fill_opacity << ","
571 << R"("font_family":")" << json_escape(style.font_family) << "\","
572 << R"("font_size":)" << style.font_size
573 << "}";
574 return oss.str();
575}
576
579 if (json.empty() || json == "{}") return style;
580
581 auto find_string_value = [&](const char* key) -> std::string {
582 std::string search = std::string("\"") + key + "\":\"";
583 auto pos = json.find(search);
584 if (pos == std::string_view::npos) return "";
585 pos += search.size();
586 auto end = json.find('"', pos);
587 if (end == std::string_view::npos) return "";
588 return std::string(json.substr(pos, end - pos));
589 };
590
591 auto find_int_value = [&](const char* key) -> int {
592 std::string search = std::string("\"") + key + "\":";
593 auto pos = json.find(search);
594 if (pos == std::string_view::npos) return 0;
595 pos += search.size();
596 return std::atoi(json.data() + pos);
597 };
598
599 auto find_float_value = [&](const char* key) -> float {
600 std::string search = std::string("\"") + key + "\":";
601 auto pos = json.find(search);
602 if (pos == std::string_view::npos) return 0.0f;
603 pos += search.size();
604 return static_cast<float>(std::atof(json.data() + pos));
605 };
606
607 auto color = find_string_value("color");
608 if (!color.empty()) style.color = color;
609
610 style.line_width = find_int_value("line_width");
611 if (style.line_width == 0) style.line_width = 2;
612
613 style.fill_color = find_string_value("fill_color");
614 style.fill_opacity = find_float_value("fill_opacity");
615
616 auto font_family = find_string_value("font_family");
617 if (!font_family.empty()) style.font_family = font_family;
618
619 style.font_size = find_int_value("font_size");
620 if (style.font_size == 0) style.font_size = 14;
621
622 return style;
623}
624
626
628
630
631auto annotation_repository::operator=(annotation_repository&&) noexcept
632 -> annotation_repository& = default;
633
634VoidResult annotation_repository::save(const annotation_record& record) {
635 if (!db_) {
636 return VoidResult(kcenon::common::error_info{
637 -1, "Database not initialized", "annotation_repository"});
638 }
639
640 static constexpr const char* sql = R"(
641 INSERT INTO annotations (
642 annotation_id, study_uid, series_uid, sop_instance_uid, frame_number,
643 user_id, annotation_type, geometry_json, text, style_json,
644 created_at, updated_at
645 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
646 ON CONFLICT(annotation_id) DO UPDATE SET
647 geometry_json = excluded.geometry_json,
648 text = excluded.text,
649 style_json = excluded.style_json,
650 updated_at = excluded.updated_at
651 )";
652
653 sqlite3_stmt* stmt = nullptr;
654 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
655 return VoidResult(kcenon::common::error_info{
656 -1, "Failed to prepare statement: " + std::string(sqlite3_errmsg(db_)),
657 "annotation_repository"});
658 }
659
660 auto now = std::chrono::system_clock::now();
661 auto now_str = to_timestamp_string(now);
662 auto style_json = serialize_style(record.style);
663
664 int idx = 1;
665 sqlite3_bind_text(stmt, idx++, record.annotation_id.c_str(), -1, SQLITE_TRANSIENT);
666 sqlite3_bind_text(stmt, idx++, record.study_uid.c_str(), -1, SQLITE_TRANSIENT);
667 sqlite3_bind_text(stmt, idx++, record.series_uid.c_str(), -1, SQLITE_TRANSIENT);
668 sqlite3_bind_text(stmt, idx++, record.sop_instance_uid.c_str(), -1, SQLITE_TRANSIENT);
669 bind_optional_int(stmt, idx++, record.frame_number);
670 sqlite3_bind_text(stmt, idx++, record.user_id.c_str(), -1, SQLITE_TRANSIENT);
671 sqlite3_bind_text(stmt, idx++, to_string(record.type).c_str(), -1, SQLITE_TRANSIENT);
672 sqlite3_bind_text(stmt, idx++, record.geometry_json.c_str(), -1, SQLITE_TRANSIENT);
673 sqlite3_bind_text(stmt, idx++, record.text.c_str(), -1, SQLITE_TRANSIENT);
674 sqlite3_bind_text(stmt, idx++, style_json.c_str(), -1, SQLITE_TRANSIENT);
675 sqlite3_bind_text(stmt, idx++, now_str.c_str(), -1, SQLITE_TRANSIENT);
676 sqlite3_bind_text(stmt, idx++, now_str.c_str(), -1, SQLITE_TRANSIENT);
677
678 auto rc = sqlite3_step(stmt);
679 sqlite3_finalize(stmt);
680
681 if (rc != SQLITE_DONE) {
682 return VoidResult(kcenon::common::error_info{
683 -1, "Failed to save annotation: " + std::string(sqlite3_errmsg(db_)),
684 "annotation_repository"});
685 }
686
687 return kcenon::common::ok();
688}
689
690std::optional<annotation_record> annotation_repository::find_by_id(
691 std::string_view annotation_id) const {
692 if (!db_) return std::nullopt;
693
694 static constexpr const char* sql = R"(
695 SELECT pk, annotation_id, study_uid, series_uid, sop_instance_uid, frame_number,
696 user_id, annotation_type, geometry_json, text, style_json,
697 created_at, updated_at
698 FROM annotations WHERE annotation_id = ?
699 )";
700
701 sqlite3_stmt* stmt = nullptr;
702 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
703 return std::nullopt;
704 }
705
706 sqlite3_bind_text(stmt, 1, annotation_id.data(),
707 static_cast<int>(annotation_id.size()), SQLITE_TRANSIENT);
708
709 std::optional<annotation_record> result;
710 if (sqlite3_step(stmt) == SQLITE_ROW) {
711 result = parse_row(stmt);
712 }
713
714 sqlite3_finalize(stmt);
715 return result;
716}
717
718std::optional<annotation_record> annotation_repository::find_by_pk(int64_t pk) const {
719 if (!db_) return std::nullopt;
720
721 static constexpr const char* sql = R"(
722 SELECT pk, annotation_id, study_uid, series_uid, sop_instance_uid, frame_number,
723 user_id, annotation_type, geometry_json, text, style_json,
724 created_at, updated_at
725 FROM annotations WHERE pk = ?
726 )";
727
728 sqlite3_stmt* stmt = nullptr;
729 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
730 return std::nullopt;
731 }
732
733 sqlite3_bind_int64(stmt, 1, pk);
734
735 std::optional<annotation_record> result;
736 if (sqlite3_step(stmt) == SQLITE_ROW) {
737 result = parse_row(stmt);
738 }
739
740 sqlite3_finalize(stmt);
741 return result;
742}
743
744std::vector<annotation_record> annotation_repository::find_by_instance(
745 std::string_view sop_instance_uid) const {
746 annotation_query query;
747 query.sop_instance_uid = std::string(sop_instance_uid);
748 return search(query);
749}
750
751std::vector<annotation_record> annotation_repository::find_by_study(
752 std::string_view study_uid) const {
753 annotation_query query;
754 query.study_uid = std::string(study_uid);
755 return search(query);
756}
757
758std::vector<annotation_record> annotation_repository::search(
759 const annotation_query& query) const {
760 std::vector<annotation_record> result;
761 if (!db_) return result;
762
763 std::ostringstream sql;
764 sql << R"(
765 SELECT pk, annotation_id, study_uid, series_uid, sop_instance_uid, frame_number,
766 user_id, annotation_type, geometry_json, text, style_json,
767 created_at, updated_at
768 FROM annotations WHERE 1=1
769 )";
770
771 std::vector<std::pair<int, std::string>> bindings;
772 int param_idx = 1;
773
774 if (query.study_uid.has_value()) {
775 sql << " AND study_uid = ?";
776 bindings.emplace_back(param_idx++, query.study_uid.value());
777 }
778
779 if (query.series_uid.has_value()) {
780 sql << " AND series_uid = ?";
781 bindings.emplace_back(param_idx++, query.series_uid.value());
782 }
783
784 if (query.sop_instance_uid.has_value()) {
785 sql << " AND sop_instance_uid = ?";
786 bindings.emplace_back(param_idx++, query.sop_instance_uid.value());
787 }
788
789 if (query.user_id.has_value()) {
790 sql << " AND user_id = ?";
791 bindings.emplace_back(param_idx++, query.user_id.value());
792 }
793
794 if (query.type.has_value()) {
795 sql << " AND annotation_type = ?";
796 bindings.emplace_back(param_idx++, to_string(query.type.value()));
797 }
798
799 sql << " ORDER BY created_at DESC";
800
801 if (query.limit > 0) {
802 sql << " LIMIT " << query.limit << " OFFSET " << query.offset;
803 }
804
805 sqlite3_stmt* stmt = nullptr;
806 auto sql_str = sql.str();
807 if (sqlite3_prepare_v2(db_, sql_str.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
808 return result;
809 }
810
811 for (const auto& [idx, value] : bindings) {
812 sqlite3_bind_text(stmt, idx, value.c_str(), -1, SQLITE_TRANSIENT);
813 }
814
815 while (sqlite3_step(stmt) == SQLITE_ROW) {
816 result.push_back(parse_row(stmt));
817 }
819 sqlite3_finalize(stmt);
820 return result;
821}
822
823VoidResult annotation_repository::update(const annotation_record& record) {
824 if (!db_) {
825 return VoidResult(kcenon::common::error_info{
826 -1, "Database not initialized", "annotation_repository"});
827 }
828
829 static constexpr const char* sql = R"(
830 UPDATE annotations SET
831 geometry_json = ?,
832 text = ?,
833 style_json = ?,
834 updated_at = ?
835 WHERE annotation_id = ?
836 )";
837
838 sqlite3_stmt* stmt = nullptr;
839 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
840 return VoidResult(kcenon::common::error_info{
841 -1, "Failed to prepare statement: " + std::string(sqlite3_errmsg(db_)),
842 "annotation_repository"});
843 }
844
845 auto now_str = to_timestamp_string(std::chrono::system_clock::now());
846 auto style_json = serialize_style(record.style);
847
848 int idx = 1;
849 sqlite3_bind_text(stmt, idx++, record.geometry_json.c_str(), -1, SQLITE_TRANSIENT);
850 sqlite3_bind_text(stmt, idx++, record.text.c_str(), -1, SQLITE_TRANSIENT);
851 sqlite3_bind_text(stmt, idx++, style_json.c_str(), -1, SQLITE_TRANSIENT);
852 sqlite3_bind_text(stmt, idx++, now_str.c_str(), -1, SQLITE_TRANSIENT);
853 sqlite3_bind_text(stmt, idx++, record.annotation_id.c_str(), -1, SQLITE_TRANSIENT);
854
855 auto rc = sqlite3_step(stmt);
856 sqlite3_finalize(stmt);
857
858 if (rc != SQLITE_DONE) {
859 return VoidResult(kcenon::common::error_info{
860 -1, "Failed to update annotation: " + std::string(sqlite3_errmsg(db_)),
861 "annotation_repository"});
863
864 return kcenon::common::ok();
865}
866
867VoidResult annotation_repository::remove(std::string_view annotation_id) {
868 if (!db_) {
869 return VoidResult(kcenon::common::error_info{
870 -1, "Database not initialized", "annotation_repository"});
871 }
872
873 static constexpr const char* sql = "DELETE FROM annotations WHERE annotation_id = ?";
874
875 sqlite3_stmt* stmt = nullptr;
876 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
877 return VoidResult(kcenon::common::error_info{
878 -1, "Failed to prepare statement: " + std::string(sqlite3_errmsg(db_)),
879 "annotation_repository"});
880 }
881
882 sqlite3_bind_text(stmt, 1, annotation_id.data(),
883 static_cast<int>(annotation_id.size()), SQLITE_TRANSIENT);
884
885 auto rc = sqlite3_step(stmt);
886 sqlite3_finalize(stmt);
887
888 if (rc != SQLITE_DONE) {
889 return VoidResult(kcenon::common::error_info{
890 -1, "Failed to delete annotation: " + std::string(sqlite3_errmsg(db_)),
891 "annotation_repository"});
893
894 return kcenon::common::ok();
895}
896
897bool annotation_repository::exists(std::string_view annotation_id) const {
898 if (!db_) return false;
899
900 static constexpr const char* sql =
901 "SELECT 1 FROM annotations WHERE annotation_id = ?";
902
903 sqlite3_stmt* stmt = nullptr;
904 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
905 return false;
906 }
907
908 sqlite3_bind_text(stmt, 1, annotation_id.data(),
909 static_cast<int>(annotation_id.size()), SQLITE_TRANSIENT);
910
911 bool found = (sqlite3_step(stmt) == SQLITE_ROW);
912 sqlite3_finalize(stmt);
913 return found;
914}
915
916size_t annotation_repository::count() const {
917 if (!db_) return 0;
918
919 static constexpr const char* sql = "SELECT COUNT(*) FROM annotations";
920
921 sqlite3_stmt* stmt = nullptr;
922 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
923 return 0;
924 }
925
926 size_t result = 0;
927 if (sqlite3_step(stmt) == SQLITE_ROW) {
928 result = static_cast<size_t>(sqlite3_column_int64(stmt, 0));
929 }
930
931 sqlite3_finalize(stmt);
932 return result;
933}
934
935size_t annotation_repository::count(const annotation_query& query) const {
936 if (!db_) return 0;
937
938 std::ostringstream sql;
939 sql << "SELECT COUNT(*) FROM annotations WHERE 1=1";
940
941 std::vector<std::pair<int, std::string>> bindings;
942 int param_idx = 1;
943
944 if (query.study_uid.has_value()) {
945 sql << " AND study_uid = ?";
946 bindings.emplace_back(param_idx++, query.study_uid.value());
947 }
948
949 if (query.series_uid.has_value()) {
950 sql << " AND series_uid = ?";
951 bindings.emplace_back(param_idx++, query.series_uid.value());
952 }
953
954 if (query.sop_instance_uid.has_value()) {
955 sql << " AND sop_instance_uid = ?";
956 bindings.emplace_back(param_idx++, query.sop_instance_uid.value());
957 }
958
959 if (query.user_id.has_value()) {
960 sql << " AND user_id = ?";
961 bindings.emplace_back(param_idx++, query.user_id.value());
962 }
963
964 if (query.type.has_value()) {
965 sql << " AND annotation_type = ?";
966 bindings.emplace_back(param_idx++, to_string(query.type.value()));
967 }
968
969 sqlite3_stmt* stmt = nullptr;
970 auto sql_str = sql.str();
971 if (sqlite3_prepare_v2(db_, sql_str.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
972 return 0;
973 }
974
975 for (const auto& [idx, value] : bindings) {
976 sqlite3_bind_text(stmt, idx, value.c_str(), -1, SQLITE_TRANSIENT);
977 }
978
979 size_t result = 0;
980 if (sqlite3_step(stmt) == SQLITE_ROW) {
981 result = static_cast<size_t>(sqlite3_column_int64(stmt, 0));
982 }
984 sqlite3_finalize(stmt);
985 return result;
986}
988bool annotation_repository::is_valid() const noexcept {
989 return db_ != nullptr;
990}
991
993 auto* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
994 annotation_record record;
995
996 int col = 0;
997 record.pk = get_int64_column(stmt, col++);
998 record.annotation_id = get_text_column(stmt, col++);
999 record.study_uid = get_text_column(stmt, col++);
1000 record.series_uid = get_text_column(stmt, col++);
1001 record.sop_instance_uid = get_text_column(stmt, col++);
1002 record.frame_number = get_optional_int(stmt, col++);
1003 record.user_id = get_text_column(stmt, col++);
1004
1005 auto type_str = get_text_column(stmt, col++);
1006 record.type = annotation_type_from_string(type_str).value_or(annotation_type::text);
1007
1008 record.geometry_json = get_text_column(stmt, col++);
1009 record.text = get_text_column(stmt, col++);
1010
1011 auto style_json = get_text_column(stmt, col++);
1012 record.style = deserialize_style(style_json);
1013
1014 auto created_str = get_text_column(stmt, col++);
1015 record.created_at = from_timestamp_string(created_str.c_str());
1016
1017 auto updated_str = get_text_column(stmt, col++);
1018 record.updated_at = from_timestamp_string(updated_str.c_str());
1019
1020 return record;
1021}
1022
1023} // namespace kcenon::pacs::storage
1024
1025#endif // PACS_WITH_DATABASE_SYSTEM
return style
auto find_int_value
auto font_family
auto find_float_value
Repository for annotation persistence using base_repository pattern.
Repository for annotation persistence (legacy SQLite interface)
auto remove(std::string_view annotation_id) -> VoidResult
auto find_by_instance(std::string_view sop_instance_uid) const -> std::vector< annotation_record >
auto update(const annotation_record &record) -> VoidResult
auto find_by_study(std::string_view study_uid) const -> std::vector< annotation_record >
static auto serialize_style(const annotation_style &style) -> std::string
auto exists(std::string_view annotation_id) const -> bool
auto find_by_id(std::string_view annotation_id) const -> std::optional< annotation_record >
static auto deserialize_style(std::string_view json) -> annotation_style
auto search(const annotation_query &query) const -> std::vector< annotation_record >
auto parse_row(void *stmt) const -> annotation_record
auto find_by_pk(int64_t pk) const -> std::optional< annotation_record >
@ move
C-MOVE move request/response.
const atna_coded_value query
Query (110112)
auto annotation_type_from_string(std::string_view str) -> std::optional< annotation_type >
Parse string to annotation_type.
auto to_string(annotation_type type) -> std::string
Convert annotation_type to string.
Annotation record from the database.
Style information for annotations.