PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
measurement_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
15
16#include <chrono>
17#include <sstream>
18
19#ifdef PACS_WITH_DATABASE_SYSTEM
20
21namespace kcenon::pacs::storage {
22
23// =============================================================================
24// Constructor
25// =============================================================================
26
28 std::shared_ptr<pacs_database_adapter> db)
29 : base_repository(std::move(db), "measurements", "measurement_id") {}
30
31// =============================================================================
32// Domain-Specific Operations
33// =============================================================================
34
35auto measurement_repository::find_by_pk(int64_t pk) -> result_type {
36 if (!db() || !db()->is_connected()) {
37 return result_type(kcenon::common::error_info{
38 -1, "Database not connected", "storage"});
39 }
40
41 auto builder = query_builder();
42 builder.select(select_columns())
43 .from(table_name())
44 .where("pk", "=", pk)
45 .limit(1);
46
47 auto result = storage_session().select(builder.build());
48 if (result.is_err()) {
49 return result_type(result.error());
50 }
51
52 if (result.value().empty()) {
53 return result_type(kcenon::common::error_info{
54 -1, "Measurement not found with pk=" + std::to_string(pk),
55 "storage"});
56 }
57
58 return result_type(map_row_to_entity(result.value()[0]));
59}
60
61auto measurement_repository::find_by_instance(std::string_view sop_instance_uid)
62 -> list_result_type {
63 return find_where("sop_instance_uid", "=", std::string(sop_instance_uid));
64}
65
66auto measurement_repository::search(const measurement_query& query)
67 -> list_result_type {
68 if (!db() || !db()->is_connected()) {
69 return list_result_type(kcenon::common::error_info{
70 -1, "Database not connected", "storage"});
71 }
72
73 auto builder = query_builder();
74 builder.select(select_columns()).from(table_name());
75
76 // Build compound condition
77 std::optional<database::query_condition> condition;
78
79 if (query.sop_instance_uid.has_value()) {
80 auto cond = database::query_condition(
81 "sop_instance_uid", "=", query.sop_instance_uid.value());
82 condition = cond;
83 }
84
85 if (query.user_id.has_value()) {
86 auto cond =
87 database::query_condition("user_id", "=", query.user_id.value());
88 if (condition.has_value()) {
89 condition = condition.value() && cond;
90 } else {
91 condition = cond;
92 }
93 }
94
95 if (query.type.has_value()) {
96 std::string type_str = to_string(query.type.value());
97 auto cond =
98 database::query_condition("measurement_type", "=", type_str);
99 if (condition.has_value()) {
100 condition = condition.value() && cond;
101 } else {
102 condition = cond;
103 }
104 }
105
106 if (condition.has_value()) {
107 builder.where(condition.value());
108 }
109
110 // Apply ordering
111 builder.order_by("created_at", database::sort_order::desc);
112
113 // Apply pagination
114 if (query.limit > 0) {
115 builder.limit(query.limit);
116 if (query.offset > 0) {
117 builder.offset(query.offset);
118 }
119 }
120
121 auto result = storage_session().select(builder.build());
122 if (result.is_err()) {
123 return list_result_type(result.error());
124 }
125
126 std::vector<measurement_record> records;
127 records.reserve(result.value().size());
128 for (const auto& row : result.value()) {
129 records.push_back(map_row_to_entity(row));
130 }
131
132 return list_result_type(std::move(records));
133}
134
135auto measurement_repository::count(const measurement_query& query)
136 -> Result<size_t> {
137 if (!db() || !db()->is_connected()) {
138 return Result<size_t>(kcenon::common::error_info{
139 -1, "Database not connected", "storage"});
140 }
141
142 auto builder = query_builder();
143 builder.select({"pk"}).from(table_name());
144
145 // Build compound condition
146 std::optional<database::query_condition> condition;
147
148 if (query.sop_instance_uid.has_value()) {
149 auto cond = database::query_condition(
150 "sop_instance_uid", "=", query.sop_instance_uid.value());
151 condition = cond;
152 }
153
154 if (query.user_id.has_value()) {
155 auto cond =
156 database::query_condition("user_id", "=", query.user_id.value());
157 if (condition.has_value()) {
158 condition = condition.value() && cond;
159 } else {
160 condition = cond;
161 }
162 }
163
164 if (query.type.has_value()) {
165 std::string type_str = to_string(query.type.value());
166 auto cond =
167 database::query_condition("measurement_type", "=", type_str);
168 if (condition.has_value()) {
169 condition = condition.value() && cond;
170 } else {
171 condition = cond;
172 }
173 }
174
175 if (condition.has_value()) {
176 builder.where(condition.value());
177 }
178
179 auto result = storage_session().select(builder.build());
180 if (result.is_err()) {
181 return Result<size_t>(result.error());
182 }
183
184 return Result<size_t>(result.value().size());
185}
186
187// =============================================================================
188// base_repository Overrides
189// =============================================================================
190
191auto measurement_repository::map_row_to_entity(const database_row& row) const
192 -> measurement_record {
193 measurement_record record;
194
195 record.pk = std::stoll(row.at("pk"));
196 record.measurement_id = row.at("measurement_id");
197 record.sop_instance_uid = row.at("sop_instance_uid");
198
199 // Handle optional frame_number
200 auto frame_it = row.find("frame_number");
201 if (frame_it != row.end() && !frame_it->second.empty()) {
202 record.frame_number = std::stoi(frame_it->second);
203 }
204
205 record.user_id = row.at("user_id");
206
207 // Parse measurement type
208 auto type_str = row.at("measurement_type");
209 record.type =
210 measurement_type_from_string(type_str).value_or(measurement_type::length);
211
212 record.geometry_json = row.at("geometry_json");
213 record.value = std::stod(row.at("value"));
214 record.unit = row.at("unit");
215 record.label = row.at("label");
216
217 // Parse created_at timestamp
218 auto created_it = row.find("created_at");
219 if (created_it != row.end() && !created_it->second.empty()) {
220 record.created_at = parse_timestamp(created_it->second);
221 }
222
223 return record;
224}
225
226auto measurement_repository::entity_to_row(
227 const measurement_record& entity) const
228 -> std::map<std::string, database_value> {
229 std::map<std::string, database_value> row;
230
231 row["measurement_id"] = entity.measurement_id;
232 row["sop_instance_uid"] = entity.sop_instance_uid;
233
234 if (entity.frame_number.has_value()) {
235 row["frame_number"] =
236 static_cast<int64_t>(entity.frame_number.value());
237 } else {
238 row["frame_number"] = nullptr;
239 }
240
241 row["user_id"] = entity.user_id;
242 row["measurement_type"] = to_string(entity.type);
243 row["geometry_json"] = entity.geometry_json;
244 row["value"] = entity.value;
245 row["unit"] = entity.unit;
246 row["label"] = entity.label;
247
248 // Format timestamp for storage
249 if (entity.created_at != std::chrono::system_clock::time_point{}) {
250 row["created_at"] = format_timestamp(entity.created_at);
251 } else {
252 row["created_at"] = format_timestamp(std::chrono::system_clock::now());
253 }
254
255 return row;
256}
257
258auto measurement_repository::get_pk(const measurement_record& entity) const
259 -> std::string {
260 return entity.measurement_id;
261}
262
263auto measurement_repository::has_pk(const measurement_record& entity) const
264 -> bool {
265 return !entity.measurement_id.empty();
266}
267
268auto measurement_repository::select_columns() const
269 -> std::vector<std::string> {
270 return {"pk",
271 "measurement_id",
272 "sop_instance_uid",
273 "frame_number",
274 "user_id",
275 "measurement_type",
276 "geometry_json",
277 "value",
278 "unit",
279 "label",
280 "created_at"};
281}
282
283// =============================================================================
284// Private Helpers
285// =============================================================================
286
287auto measurement_repository::parse_timestamp(const std::string& str) const
288 -> std::chrono::system_clock::time_point {
289 if (str.empty()) {
290 return {};
291 }
292
293 std::tm tm{};
294 if (std::sscanf(str.c_str(), "%d-%d-%d %d:%d:%d", &tm.tm_year, &tm.tm_mon,
295 &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) != 6) {
296 return {};
297 }
298
299 tm.tm_year -= 1900;
300 tm.tm_mon -= 1;
301
302#ifdef _WIN32
303 auto time = _mkgmtime(&tm);
304#else
305 auto time = timegm(&tm);
306#endif
307
308 return std::chrono::system_clock::from_time_t(time);
309}
310
311auto measurement_repository::format_timestamp(
312 std::chrono::system_clock::time_point tp) const -> std::string {
313 if (tp == std::chrono::system_clock::time_point{}) {
314 return "";
315 }
316
317 auto time = std::chrono::system_clock::to_time_t(tp);
318 std::tm tm{};
319#ifdef _WIN32
320 gmtime_s(&tm, &time);
321#else
322 gmtime_r(&time, &tm);
323#endif
324
325 char buf[32];
326 std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm);
327 return buf;
328}
329
330} // namespace kcenon::pacs::storage
331
332#else // !PACS_WITH_DATABASE_SYSTEM
333
334// =============================================================================
335// Legacy SQLite Implementation
336// =============================================================================
337
338#include <sqlite3.h>
339
340namespace kcenon::pacs::storage {
341
342namespace {
343
344[[nodiscard]] std::string to_timestamp_string(
345 std::chrono::system_clock::time_point tp) {
346 if (tp == std::chrono::system_clock::time_point{}) {
347 return "";
348 }
349 auto time = std::chrono::system_clock::to_time_t(tp);
350 std::tm tm{};
351#ifdef _WIN32
352 gmtime_s(&tm, &time);
353#else
354 gmtime_r(&time, &tm);
355#endif
356 char buf[32];
357 std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm);
358 return buf;
359}
360
361[[nodiscard]] std::chrono::system_clock::time_point from_timestamp_string(
362 const char* str) {
363 if (!str || str[0] == '\0') {
364 return {};
365 }
366 std::tm tm{};
367 if (std::sscanf(str, "%d-%d-%d %d:%d:%d", &tm.tm_year, &tm.tm_mon,
368 &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) != 6) {
369 return {};
370 }
371 tm.tm_year -= 1900;
372 tm.tm_mon -= 1;
373#ifdef _WIN32
374 auto time = _mkgmtime(&tm);
375#else
376 auto time = timegm(&tm);
377#endif
378 return std::chrono::system_clock::from_time_t(time);
379}
380
381[[nodiscard]] std::string get_text_column(sqlite3_stmt* stmt, int col) {
382 auto text = reinterpret_cast<const char*>(sqlite3_column_text(stmt, col));
383 return text ? text : "";
384}
385
386[[nodiscard]] int64_t get_int64_column(sqlite3_stmt* stmt, int col,
387 int64_t default_val = 0) {
388 if (sqlite3_column_type(stmt, col) == SQLITE_NULL) {
389 return default_val;
390 }
391 return sqlite3_column_int64(stmt, col);
392}
393
394[[nodiscard]] double get_double_column(sqlite3_stmt* stmt, int col,
395 double default_val = 0.0) {
396 if (sqlite3_column_type(stmt, col) == SQLITE_NULL) {
397 return default_val;
398 }
399 return sqlite3_column_double(stmt, col);
400}
401
402[[nodiscard]] std::optional<int> get_optional_int(sqlite3_stmt* stmt, int col) {
403 if (sqlite3_column_type(stmt, col) == SQLITE_NULL) {
404 return std::nullopt;
405 }
406 return sqlite3_column_int(stmt, col);
407}
408
409void bind_optional_int(sqlite3_stmt* stmt, int idx,
410 const std::optional<int>& value) {
411 if (value.has_value()) {
412 sqlite3_bind_int(stmt, idx, value.value());
413 } else {
414 sqlite3_bind_null(stmt, idx);
415 }
416}
417
418} // namespace
419
421
423
425 measurement_repository&&) noexcept = default;
426
427auto measurement_repository::operator=(measurement_repository&&) noexcept
428 -> measurement_repository& = default;
429
430VoidResult measurement_repository::save(const measurement_record& record) {
431 if (!db_) {
432 return VoidResult(kcenon::common::error_info{
433 -1, "Database not initialized", "measurement_repository"});
434 }
435
436 static constexpr const char* sql = R"(
437 INSERT INTO measurements (
438 measurement_id, sop_instance_uid, frame_number, user_id,
439 measurement_type, geometry_json, value, unit, label, created_at
440 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
441 ON CONFLICT(measurement_id) DO UPDATE SET
442 geometry_json = excluded.geometry_json,
443 value = excluded.value,
444 unit = excluded.unit,
445 label = excluded.label
446 )";
447
448 sqlite3_stmt* stmt = nullptr;
449 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
450 return VoidResult(kcenon::common::error_info{
451 -1,
452 "Failed to prepare statement: " + std::string(sqlite3_errmsg(db_)),
453 "measurement_repository"});
454 }
455
456 auto now_str = to_timestamp_string(std::chrono::system_clock::now());
457
458 int idx = 1;
459 sqlite3_bind_text(stmt, idx++, record.measurement_id.c_str(), -1,
460 SQLITE_TRANSIENT);
461 sqlite3_bind_text(stmt, idx++, record.sop_instance_uid.c_str(), -1,
462 SQLITE_TRANSIENT);
463 bind_optional_int(stmt, idx++, record.frame_number);
464 sqlite3_bind_text(stmt, idx++, record.user_id.c_str(), -1, SQLITE_TRANSIENT);
465 sqlite3_bind_text(stmt, idx++, to_string(record.type).c_str(), -1,
466 SQLITE_TRANSIENT);
467 sqlite3_bind_text(stmt, idx++, record.geometry_json.c_str(), -1,
468 SQLITE_TRANSIENT);
469 sqlite3_bind_double(stmt, idx++, record.value);
470 sqlite3_bind_text(stmt, idx++, record.unit.c_str(), -1, SQLITE_TRANSIENT);
471 sqlite3_bind_text(stmt, idx++, record.label.c_str(), -1, SQLITE_TRANSIENT);
472 sqlite3_bind_text(stmt, idx++, now_str.c_str(), -1, SQLITE_TRANSIENT);
473
474 auto rc = sqlite3_step(stmt);
475 sqlite3_finalize(stmt);
476
477 if (rc != SQLITE_DONE) {
478 return VoidResult(kcenon::common::error_info{
479 -1,
480 "Failed to save measurement: " + std::string(sqlite3_errmsg(db_)),
481 "measurement_repository"});
482 }
483
484 return kcenon::common::ok();
485}
486
487std::optional<measurement_record> measurement_repository::find_by_id(
488 std::string_view measurement_id) const {
489 if (!db_) return std::nullopt;
490
491 static constexpr const char* sql = R"(
492 SELECT pk, measurement_id, sop_instance_uid, frame_number, user_id,
493 measurement_type, geometry_json, value, unit, label, created_at
494 FROM measurements WHERE measurement_id = ?
495 )";
496
497 sqlite3_stmt* stmt = nullptr;
498 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
499 return std::nullopt;
500 }
501
502 sqlite3_bind_text(stmt, 1, measurement_id.data(),
503 static_cast<int>(measurement_id.size()), SQLITE_TRANSIENT);
504
505 std::optional<measurement_record> result;
506 if (sqlite3_step(stmt) == SQLITE_ROW) {
507 result = parse_row(stmt);
508 }
509
510 sqlite3_finalize(stmt);
511 return result;
512}
513
514std::optional<measurement_record> measurement_repository::find_by_pk(
515 int64_t pk) const {
516 if (!db_) return std::nullopt;
517
518 static constexpr const char* sql = R"(
519 SELECT pk, measurement_id, sop_instance_uid, frame_number, user_id,
520 measurement_type, geometry_json, value, unit, label, created_at
521 FROM measurements WHERE pk = ?
522 )";
523
524 sqlite3_stmt* stmt = nullptr;
525 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
526 return std::nullopt;
527 }
528
529 sqlite3_bind_int64(stmt, 1, pk);
530
531 std::optional<measurement_record> result;
532 if (sqlite3_step(stmt) == SQLITE_ROW) {
533 result = parse_row(stmt);
534 }
535
536 sqlite3_finalize(stmt);
537 return result;
538}
539
540std::vector<measurement_record> measurement_repository::find_by_instance(
541 std::string_view sop_instance_uid) const {
542 measurement_query query;
543 query.sop_instance_uid = std::string(sop_instance_uid);
544 return search(query);
545}
546
547std::vector<measurement_record> measurement_repository::search(
548 const measurement_query& query) const {
549 std::vector<measurement_record> result;
550 if (!db_) return result;
551
552 std::ostringstream sql;
553 sql << R"(
554 SELECT pk, measurement_id, sop_instance_uid, frame_number, user_id,
555 measurement_type, geometry_json, value, unit, label, created_at
556 FROM measurements WHERE 1=1
557 )";
558
559 std::vector<std::pair<int, std::string>> bindings;
560 int param_idx = 1;
561
562 if (query.sop_instance_uid.has_value()) {
563 sql << " AND sop_instance_uid = ?";
564 bindings.emplace_back(param_idx++, query.sop_instance_uid.value());
565 }
566
567 if (query.user_id.has_value()) {
568 sql << " AND user_id = ?";
569 bindings.emplace_back(param_idx++, query.user_id.value());
570 }
571
572 if (query.type.has_value()) {
573 sql << " AND measurement_type = ?";
574 bindings.emplace_back(param_idx++, to_string(query.type.value()));
575 }
576
577 sql << " ORDER BY created_at DESC";
578
579 if (query.limit > 0) {
580 sql << " LIMIT " << query.limit << " OFFSET " << query.offset;
581 }
582
583 sqlite3_stmt* stmt = nullptr;
584 auto sql_str = sql.str();
585 if (sqlite3_prepare_v2(db_, sql_str.c_str(), -1, &stmt, nullptr) !=
586 SQLITE_OK) {
587 return result;
588 }
589
590 for (const auto& [idx, value] : bindings) {
591 sqlite3_bind_text(stmt, idx, value.c_str(), -1, SQLITE_TRANSIENT);
592 }
593
594 while (sqlite3_step(stmt) == SQLITE_ROW) {
595 result.push_back(parse_row(stmt));
596 }
597
598 sqlite3_finalize(stmt);
599 return result;
600}
601
602VoidResult measurement_repository::remove(std::string_view measurement_id) {
603 if (!db_) {
604 return VoidResult(kcenon::common::error_info{
605 -1, "Database not initialized", "measurement_repository"});
606 }
607
608 static constexpr const char* sql =
609 "DELETE FROM measurements WHERE measurement_id = ?";
610
611 sqlite3_stmt* stmt = nullptr;
612 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
613 return VoidResult(kcenon::common::error_info{
614 -1,
615 "Failed to prepare statement: " + std::string(sqlite3_errmsg(db_)),
616 "measurement_repository"});
617 }
618
619 sqlite3_bind_text(stmt, 1, measurement_id.data(),
620 static_cast<int>(measurement_id.size()), SQLITE_TRANSIENT);
621
622 auto rc = sqlite3_step(stmt);
623 sqlite3_finalize(stmt);
624
625 if (rc != SQLITE_DONE) {
626 return VoidResult(kcenon::common::error_info{
627 -1,
628 "Failed to delete measurement: " + std::string(sqlite3_errmsg(db_)),
629 "measurement_repository"});
630 }
632 return kcenon::common::ok();
633}
634
635bool measurement_repository::exists(std::string_view measurement_id) const {
636 if (!db_) return false;
637
638 static constexpr const char* sql =
639 "SELECT 1 FROM measurements WHERE measurement_id = ?";
640
641 sqlite3_stmt* stmt = nullptr;
642 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
643 return false;
644 }
645
646 sqlite3_bind_text(stmt, 1, measurement_id.data(),
647 static_cast<int>(measurement_id.size()), SQLITE_TRANSIENT);
648
649 bool found = (sqlite3_step(stmt) == SQLITE_ROW);
650 sqlite3_finalize(stmt);
651 return found;
652}
653
654size_t measurement_repository::count() const {
655 if (!db_) return 0;
656
657 static constexpr const char* sql = "SELECT COUNT(*) FROM measurements";
658
659 sqlite3_stmt* stmt = nullptr;
660 if (sqlite3_prepare_v2(db_, sql, -1, &stmt, nullptr) != SQLITE_OK) {
661 return 0;
662 }
663
664 size_t result = 0;
665 if (sqlite3_step(stmt) == SQLITE_ROW) {
666 result = static_cast<size_t>(sqlite3_column_int64(stmt, 0));
667 }
668
669 sqlite3_finalize(stmt);
670 return result;
671}
672
673size_t measurement_repository::count(const measurement_query& query) const {
674 if (!db_) return 0;
675
676 std::ostringstream sql;
677 sql << "SELECT COUNT(*) FROM measurements WHERE 1=1";
678
679 std::vector<std::pair<int, std::string>> bindings;
680 int param_idx = 1;
681
682 if (query.sop_instance_uid.has_value()) {
683 sql << " AND sop_instance_uid = ?";
684 bindings.emplace_back(param_idx++, query.sop_instance_uid.value());
685 }
686
687 if (query.user_id.has_value()) {
688 sql << " AND user_id = ?";
689 bindings.emplace_back(param_idx++, query.user_id.value());
690 }
691
692 if (query.type.has_value()) {
693 sql << " AND measurement_type = ?";
694 bindings.emplace_back(param_idx++, to_string(query.type.value()));
695 }
696
697 sqlite3_stmt* stmt = nullptr;
698 auto sql_str = sql.str();
699 if (sqlite3_prepare_v2(db_, sql_str.c_str(), -1, &stmt, nullptr) !=
700 SQLITE_OK) {
701 return 0;
702 }
703
704 for (const auto& [idx, value] : bindings) {
705 sqlite3_bind_text(stmt, idx, value.c_str(), -1, SQLITE_TRANSIENT);
706 }
707
708 size_t result = 0;
709 if (sqlite3_step(stmt) == SQLITE_ROW) {
710 result = static_cast<size_t>(sqlite3_column_int64(stmt, 0));
711 }
712
713 sqlite3_finalize(stmt);
714 return result;
715}
716
717bool measurement_repository::is_valid() const noexcept {
718 return db_ != nullptr;
719}
720
722 auto* stmt = static_cast<sqlite3_stmt*>(stmt_ptr);
723 measurement_record record;
724
725 int col = 0;
726 record.pk = get_int64_column(stmt, col++);
727 record.measurement_id = get_text_column(stmt, col++);
728 record.sop_instance_uid = get_text_column(stmt, col++);
729 record.frame_number = get_optional_int(stmt, col++);
730 record.user_id = get_text_column(stmt, col++);
731
732 auto type_str = get_text_column(stmt, col++);
733 record.type =
735
736 record.geometry_json = get_text_column(stmt, col++);
737 record.value = get_double_column(stmt, col++);
738 record.unit = get_text_column(stmt, col++);
739 record.label = get_text_column(stmt, col++);
740
741 auto created_str = get_text_column(stmt, col++);
742 record.created_at = from_timestamp_string(created_str.c_str());
743
744 return record;
745}
746
747} // namespace kcenon::pacs::storage
748
749#endif // PACS_WITH_DATABASE_SYSTEM
Repository for measurement persistence (legacy SQLite interface)
auto find_by_pk(int64_t pk) const -> std::optional< measurement_record >
auto find_by_id(std::string_view measurement_id) const -> std::optional< measurement_record >
auto find_by_instance(std::string_view sop_instance_uid) const -> std::vector< measurement_record >
auto remove(std::string_view measurement_id) -> VoidResult
auto search(const measurement_query &query) const -> std::vector< measurement_record >
auto parse_row(void *stmt) const -> measurement_record
auto exists(std::string_view measurement_id) const -> bool
Repository for measurement persistence using base_repository pattern.
@ move
C-MOVE move request/response.
const atna_coded_value query
Query (110112)
auto measurement_type_from_string(std::string_view str) -> std::optional< measurement_type >
Parse string to measurement_type.
@ length
Linear distance measurement.
auto to_string(annotation_type type) -> std::string
Convert annotation_type to string.
Measurement record from the database.