Mercurial > hg > orthanc
view OrthancFramework/Sources/DicomNetworking/DicomControlUserConnection.cpp @ 5853:4d932683049d get-scu tip
very first implementation of C-Get SCU
author | Alain Mazy <am@orthanc.team> |
---|---|
date | Tue, 29 Oct 2024 17:25:49 +0100 |
parents | f7adfb22e20e |
children |
line wrap: on
line source
/** * Orthanc - A Lightweight, RESTful DICOM Store * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics * Department, University Hospital of Liege, Belgium * Copyright (C) 2017-2023 Osimis S.A., Belgium * Copyright (C) 2024-2024 Orthanc Team SRL, Belgium * Copyright (C) 2021-2024 Sebastien Jodogne, ICTEAM UCLouvain, Belgium * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this program. If not, see * <http://www.gnu.org/licenses/>. **/ #include "../PrecompiledHeaders.h" #include "DicomControlUserConnection.h" #include "../Compatibility.h" #include "../DicomFormat/DicomArray.h" #include "../DicomParsing/FromDcmtkBridge.h" #include "../Logging.h" #include "../OrthancException.h" #include "DicomAssociation.h" #include <dcmtk/dcmdata/dcdeftag.h> #include <dcmtk/dcmnet/diutil.h> namespace Orthanc { static void TestAndCopyTag(DicomMap& result, const DicomMap& source, const DicomTag& tag) { if (!source.HasTag(tag)) { throw OrthancException(ErrorCode_BadRequest, "Missing tag " + tag.Format()); } else { result.SetValue(tag, source.GetValue(tag)); } } namespace { struct FindPayload { DicomFindAnswers* answers; const char* level; bool isWorklist; }; } static void FindCallback( /* in */ void *callbackData, T_DIMSE_C_FindRQ *request, /* original find request */ int responseCount, T_DIMSE_C_FindRSP *response, /* pending response received */ DcmDataset *responseIdentifiers /* pending response identifiers */ ) { if (response != NULL) { OFString str; CLOG(TRACE, DICOM) << "Received Find Response " << responseCount << ":" << std::endl << DIMSE_dumpMessage(str, *response, DIMSE_INCOMING); } if (responseIdentifiers != NULL) { std::stringstream s; // DcmObject::PrintHelper cannot be used with VS2008 responseIdentifiers->print(s); CLOG(TRACE, DICOM) << "Response Identifiers " << responseCount << ":" << std::endl << s.str(); } if (responseIdentifiers != NULL) { FindPayload& payload = *reinterpret_cast<FindPayload*>(callbackData); if (payload.isWorklist) { const ParsedDicomFile answer(*responseIdentifiers); payload.answers->Add(answer); } else { DicomMap m; std::set<DicomTag> ignoreTagLength; FromDcmtkBridge::ExtractDicomSummary(m, *responseIdentifiers, 0 /* don't truncate tags */, ignoreTagLength); if (!m.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL)) { m.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, payload.level, false); } payload.answers->Add(m); } } } static void NormalizeFindQuery(DicomMap& fixedQuery, ResourceType level, const DicomMap& fields) { std::set<DicomTag> allowedTags; // WARNING: Do not add "break" or reorder items in this switch-case! switch (level) { case ResourceType_Instance: DicomTag::AddTagsForModule(allowedTags, DicomModule_Instance); case ResourceType_Series: DicomTag::AddTagsForModule(allowedTags, DicomModule_Series); case ResourceType_Study: DicomTag::AddTagsForModule(allowedTags, DicomModule_Study); case ResourceType_Patient: DicomTag::AddTagsForModule(allowedTags, DicomModule_Patient); break; default: throw OrthancException(ErrorCode_InternalError); } switch (level) { case ResourceType_Patient: allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES); allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES); allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES); break; case ResourceType_Study: allowedTags.insert(DICOM_TAG_MODALITIES_IN_STUDY); allowedTags.insert(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES); allowedTags.insert(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES); allowedTags.insert(DICOM_TAG_SOP_CLASSES_IN_STUDY); break; case ResourceType_Series: allowedTags.insert(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES); break; default: break; } allowedTags.insert(DICOM_TAG_SPECIFIC_CHARACTER_SET); DicomArray query(fields); for (size_t i = 0; i < query.GetSize(); i++) { const DicomTag& tag = query.GetElement(i).GetTag(); if (allowedTags.find(tag) == allowedTags.end()) { CLOG(WARNING, DICOM) << "Tag not allowed for this C-Find level, will be ignored: (" << tag.Format() << ")"; } else { fixedQuery.SetValue(tag, query.GetElement(i).GetValue()); } } } static ParsedDicomFile* ConvertQueryFields(const DicomMap& fields, ModalityManufacturer manufacturer) { // Fix outgoing C-Find requests issue for Syngo.Via and its // solution was reported by Emsy Chan by private mail on // 2015-06-17. According to Robert van Ommen (2015-11-30), the // same fix is required for Agfa Impax. This was generalized for // generic manufacturer since it seems to affect PhilipsADW, // GEWAServer as well: // https://orthanc.uclouvain.be/bugs/show_bug.cgi?id=31 switch (manufacturer) { case ModalityManufacturer_GenericNoWildcardInDates: case ModalityManufacturer_GenericNoUniversalWildcard: { std::unique_ptr<DicomMap> fix(fields.Clone()); std::set<DicomTag> tags; fix->GetTags(tags); for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it) { // Replace a "*" wildcard query by an empty query ("") for // "date" or "all" value representations depending on the // type of manufacturer. if (manufacturer == ModalityManufacturer_GenericNoUniversalWildcard || (manufacturer == ModalityManufacturer_GenericNoWildcardInDates && FromDcmtkBridge::LookupValueRepresentation(*it) == ValueRepresentation_Date)) { const DicomValue* value = fix->TestAndGetValue(*it); if (value != NULL && !value->IsNull() && value->GetContent() == "*") { fix->SetValue(*it, "", false); } } } return new ParsedDicomFile(*fix, GetDefaultDicomEncoding(), false /* be strict */); } default: return new ParsedDicomFile(fields, GetDefaultDicomEncoding(), false /* be strict */); } } void DicomControlUserConnection::SetupPresentationContexts() // TODO-GET, setup only the presentation contexts that are enabled for that modality { assert(association_.get() != NULL); association_->ProposeGenericPresentationContext(UID_VerificationSOPClass); association_->ProposeGenericPresentationContext(UID_FINDPatientRootQueryRetrieveInformationModel); association_->ProposeGenericPresentationContext(UID_MOVEPatientRootQueryRetrieveInformationModel); association_->ProposeGenericPresentationContext(UID_FINDStudyRootQueryRetrieveInformationModel); association_->ProposeGenericPresentationContext(UID_MOVEStudyRootQueryRetrieveInformationModel); association_->ProposeGenericPresentationContext(UID_FINDModalityWorklistInformationModel); association_->ProposeGenericPresentationContext(UID_GETStudyRootQueryRetrieveInformationModel); association_->ProposeGenericPresentationContext(UID_GETPatientRootQueryRetrieveInformationModel); // for C-GET SCU, in order to receive the C-Store message TODO-GET: we need to refine this list based on what we know we are going to retrieve association_->ProposeGenericPresentationContext(UID_ComputedRadiographyImageStorage); association_->ProposeGenericPresentationContext(UID_MRImageStorage); } void DicomControlUserConnection::FindInternal(DicomFindAnswers& answers, DcmDataset* dataset, const char* sopClass, bool isWorklist, const char* level) { assert(dataset != NULL); assert(isWorklist ^ (level != NULL)); assert(association_.get() != NULL); association_->Open(parameters_); FindPayload payload; payload.answers = &answers; payload.level = level; payload.isWorklist = isWorklist; // Figure out which of the accepted presentation contexts should be used int presID = ASC_findAcceptedPresentationContextID( &association_->GetDcmtkAssociation(), sopClass); if (presID == 0) { throw OrthancException(ErrorCode_DicomFindUnavailable, "Remote AET is " + parameters_.GetRemoteModality().GetApplicationEntityTitle()); } T_DIMSE_C_FindRQ request; memset(&request, 0, sizeof(request)); request.MessageID = association_->GetDcmtkAssociation().nextMsgID++; strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN); request.Priority = DIMSE_PRIORITY_MEDIUM; request.DataSetType = DIMSE_DATASET_PRESENT; T_DIMSE_C_FindRSP response; DcmDataset* statusDetail = NULL; #if DCMTK_VERSION_NUMBER >= 364 int responseCount; #endif { std::stringstream s; // DcmObject::PrintHelper cannot be used with VS2008 dataset->print(s); OFString str; CLOG(TRACE, DICOM) << "Sending Find Request:" << std::endl << DIMSE_dumpMessage(str, request, DIMSE_OUTGOING, NULL, presID) << std::endl << s.str(); } OFCondition cond = DIMSE_findUser( &association_->GetDcmtkAssociation(), presID, &request, dataset, #if DCMTK_VERSION_NUMBER >= 364 responseCount, #endif FindCallback, &payload, /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), /*opt_dimse_timeout*/ parameters_.GetTimeout(), &response, &statusDetail); if (statusDetail) { delete statusDetail; } DicomAssociation::CheckCondition(cond, parameters_, "C-FIND"); { OFString str; CLOG(TRACE, DICOM) << "Received Final Find Response:" << std::endl << DIMSE_dumpMessage(str, response, DIMSE_INCOMING); } /** * New in Orthanc 1.6.0: Deal with failures during C-FIND. * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.html#table_C.4-1 **/ if (response.DimseStatus != 0x0000 && // Success response.DimseStatus != 0xFF00 && // Pending - Matches are continuing response.DimseStatus != 0xFF01) // Pending - Matches are continuing { char buf[16]; sprintf(buf, "%04X", response.DimseStatus); if (response.DimseStatus == STATUS_FIND_Failed_UnableToProcess) { throw OrthancException(ErrorCode_NetworkProtocol, HttpStatus_422_UnprocessableEntity, "C-FIND SCU to AET \"" + parameters_.GetRemoteModality().GetApplicationEntityTitle() + "\" has failed with DIMSE status 0x" + buf + " (unable to process - invalid query ?)"); } else { throw OrthancException(ErrorCode_NetworkProtocol, "C-FIND SCU to AET \"" + parameters_.GetRemoteModality().GetApplicationEntityTitle() + "\" has failed with DIMSE status 0x" + buf); } } } void DicomControlUserConnection::MoveInternal(const std::string& targetAet, ResourceType level, const DicomMap& fields) { assert(association_.get() != NULL); association_->Open(parameters_); std::unique_ptr<ParsedDicomFile> query( ConvertQueryFields(fields, parameters_.GetRemoteModality().GetManufacturer())); DcmDataset* dataset = query->GetDcmtkObject().getDataset(); const char* sopClass = UID_MOVEStudyRootQueryRetrieveInformationModel; DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, ResourceTypeToDicomQueryRetrieveLevel(level)); // Figure out which of the accepted presentation contexts should be used int presID = ASC_findAcceptedPresentationContextID(&association_->GetDcmtkAssociation(), sopClass); if (presID == 0) { throw OrthancException(ErrorCode_DicomMoveUnavailable, "Remote AET is " + parameters_.GetRemoteModality().GetApplicationEntityTitle()); } T_DIMSE_C_MoveRQ request; memset(&request, 0, sizeof(request)); request.MessageID = association_->GetDcmtkAssociation().nextMsgID++; strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN); request.Priority = DIMSE_PRIORITY_MEDIUM; request.DataSetType = DIMSE_DATASET_PRESENT; strncpy(request.MoveDestination, targetAet.c_str(), DIC_AE_LEN); { OFString str; CLOG(TRACE, DICOM) << "Sending Move Request:" << std::endl << DIMSE_dumpMessage(str, request, DIMSE_OUTGOING, NULL, presID); } T_DIMSE_C_MoveRSP response; DcmDataset* statusDetail = NULL; DcmDataset* responseIdentifiers = NULL; OFCondition cond = DIMSE_moveUser( &association_->GetDcmtkAssociation(), presID, &request, dataset, /*moveCallback*/ NULL, NULL, /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), /*opt_dimse_timeout*/ parameters_.GetTimeout(), &association_->GetDcmtkNetwork(), /*subOpCallback*/ NULL, NULL, &response, &statusDetail, &responseIdentifiers); if (statusDetail) { delete statusDetail; } if (responseIdentifiers) { delete responseIdentifiers; } DicomAssociation::CheckCondition(cond, parameters_, "C-MOVE"); { OFString str; CLOG(TRACE, DICOM) << "Received Final Move Response:" << std::endl << DIMSE_dumpMessage(str, response, DIMSE_INCOMING); } /** * New in Orthanc 1.6.0: Deal with failures during C-MOVE. * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.2.html#table_C.4-2 **/ if (response.DimseStatus != 0x0000 && // Success response.DimseStatus != 0xFF00) // Pending - Sub-operations are continuing { char buf[16]; sprintf(buf, "%04X", response.DimseStatus); if (response.DimseStatus == STATUS_MOVE_Failed_UnableToProcess) { throw OrthancException(ErrorCode_NetworkProtocol, HttpStatus_422_UnprocessableEntity, "C-MOVE SCU to AET \"" + parameters_.GetRemoteModality().GetApplicationEntityTitle() + "\" has failed with DIMSE status 0x" + buf + " (unable to process - resource not found ?)"); } else { throw OrthancException(ErrorCode_NetworkProtocol, "C-MOVE SCU to AET \"" + parameters_.GetRemoteModality().GetApplicationEntityTitle() + "\" has failed with DIMSE status 0x" + buf); } } } void DicomControlUserConnection::Get(const DicomMap& findResult, CGetInstanceReceivedCallback instanceReceivedCallback, void* callbackContext) { assert(association_.get() != NULL); association_->Open(parameters_); // TODO-GET: if findResults is the result of a C-Find, we can use the SopClassUIDs for the negotiation std::unique_ptr<ParsedDicomFile> query( ConvertQueryFields(findResult, parameters_.GetRemoteModality().GetManufacturer())); DcmDataset* queryDataset = query->GetDcmtkObject().getDataset(); std::string remoteAet; std::string remoteIp; std::string calledAet; association_->GetAssociationParameters(remoteAet, remoteIp, calledAet); const char* sopClass = NULL; const std::string tmp = findResult.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).GetContent(); ResourceType level = StringToResourceType(tmp.c_str()); switch (level) { case ResourceType_Patient: sopClass = UID_GETPatientRootQueryRetrieveInformationModel; // DU_putStringDOElement(queryDataset, DCM_QueryRetrieveLevel, ResourceTypeToDicomQueryRetrieveLevel(ResourceType_Patient)); // TODO-GET break; case ResourceType_Study: sopClass = UID_GETStudyRootQueryRetrieveInformationModel; // DU_putStringDOElement(queryDataset, DCM_QueryRetrieveLevel, ResourceTypeToDicomQueryRetrieveLevel(ResourceType_Study)); // TODO-GET break; default: throw OrthancException(ErrorCode_InternalError); // TODO-GET: implement series + instances } // Figure out which of the accepted presentation contexts should be used int cgetPresID = ASC_findAcceptedPresentationContextID(&association_->GetDcmtkAssociation(), sopClass); if (cgetPresID == 0) { throw OrthancException(ErrorCode_DicomGetUnavailable, "Remote AET is " + parameters_.GetRemoteModality().GetApplicationEntityTitle()); } T_DIMSE_Message msgGetRequest; memset((char*)&msgGetRequest, 0, sizeof(msgGetRequest)); msgGetRequest.CommandField = DIMSE_C_GET_RQ; T_DIMSE_C_GetRQ* request = &(msgGetRequest.msg.CGetRQ); request->MessageID = association_->GetDcmtkAssociation().nextMsgID++; strncpy(request->AffectedSOPClassUID, sopClass, DIC_UI_LEN); request->Priority = DIMSE_PRIORITY_MEDIUM; request->DataSetType = DIMSE_DATASET_PRESENT; { OFString str; CLOG(TRACE, DICOM) << "Sending Get Request:" << std::endl << DIMSE_dumpMessage(str, *request, DIMSE_OUTGOING, NULL, cgetPresID); } OFCondition cond = DIMSE_sendMessageUsingMemoryData( &(association_->GetDcmtkAssociation()), cgetPresID, &msgGetRequest, NULL /* statusDetail */, queryDataset, NULL /* progress callback TODO-GET */, NULL /* callback context */, NULL /* commandSet */); if (cond.bad()) { OFString tempStr; CLOG(TRACE, DICOM) << "Failed sending C-GET request: " << DimseCondition::dump(tempStr, cond); // return cond; } // equivalent to handleCGETSession in DCMTK bool continueSession = true; // As long we want to continue (usually, as long as we receive more objects, // i.e. the final C-GET response has not arrived yet) while (continueSession) { T_DIMSE_Message rsp; // Make sure everything is zeroed (especially options) memset((char*)&rsp, 0, sizeof(rsp)); // DcmDataset* statusDetail = NULL; T_ASC_PresentationContextID cmdPresId = 0; OFCondition result = DIMSE_receiveCommand(&(association_->GetDcmtkAssociation()), (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), parameters_.GetTimeout(), &cmdPresId, &rsp, NULL /* statusDetail */, NULL /* not interested in the command set */); if (result.bad()) { OFString tempStr; CLOG(TRACE, DICOM) << "Failed receiving DIMSE command: " << DimseCondition::dump(tempStr, result); // delete statusDetail; break; // TODO: return value } // Handle C-GET Response if (rsp.CommandField == DIMSE_C_GET_RSP) { { OFString tempStr; CLOG(TRACE, DICOM) << "Received C-GET Response: " << std::endl << DIMSE_dumpMessage(tempStr, rsp, DIMSE_INCOMING, NULL, cmdPresId); } // TODO-GET: for progress handler // OFunique_ptr<RetrieveResponse> getRSP(new RetrieveResponse()); // getRSP->m_affectedSOPClassUID = rsp.msg.CGetRSP.AffectedSOPClassUID; // getRSP->m_messageIDRespondedTo = rsp.msg.CGetRSP.MessageIDBeingRespondedTo; // getRSP->m_status = rsp.msg.CGetRSP.DimseStatus; // getRSP->m_numberOfRemainingSubops = rsp.msg.CGetRSP.NumberOfRemainingSubOperations; // getRSP->m_numberOfCompletedSubops = rsp.msg.CGetRSP.NumberOfCompletedSubOperations; // getRSP->m_numberOfFailedSubops = rsp.msg.CGetRSP.NumberOfFailedSubOperations; // getRSP->m_numberOfWarningSubops = rsp.msg.CGetRSP.NumberOfWarningSubOperations; // getRSP->m_statusDetail = statusDetail; } // Handle C-STORE Request else if (rsp.CommandField == DIMSE_C_STORE_RQ) { { OFString tempStr; CLOG(TRACE, DICOM) << "Received C-STORE Request: " << std::endl << DIMSE_dumpMessage(tempStr, rsp, DIMSE_INCOMING, NULL, cmdPresId); } T_DIMSE_C_StoreRQ* storeRequest = &(rsp.msg.CStoreRQ); // Check if dataset is announced correctly if (rsp.msg.CStoreRQ.DataSetType == DIMSE_DATASET_NULL) { CLOG(WARNING, DICOM) << "C-GET SCU handler: Incoming C-STORE with no dataset"; } Uint16 desiredCStoreReturnStatus = 0; DcmDataset* dataObject = NULL; // Receive dataset result = DIMSE_receiveDataSetInMemory(&(association_->GetDcmtkAssociation()), (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), parameters_.GetTimeout(), &cmdPresId, &dataObject, NULL /*callback*/, NULL /*callbackData*/); // TODO-GET if (result.bad()) { desiredCStoreReturnStatus = STATUS_STORE_Error_CannotUnderstand; // TODO-GET: return ? } else { // callback the OrthancServer with the received data if (instanceReceivedCallback != NULL) { desiredCStoreReturnStatus = instanceReceivedCallback(callbackContext, *dataObject, remoteAet, remoteIp, calledAet); } // send the Store response T_DIMSE_Message storeResponse; memset((char*)&storeResponse, 0, sizeof(storeResponse)); storeResponse.CommandField = DIMSE_C_STORE_RSP; T_DIMSE_C_StoreRSP& storeRsp = storeResponse.msg.CStoreRSP; storeRsp.MessageIDBeingRespondedTo = storeRequest->MessageID; storeRsp.DimseStatus = desiredCStoreReturnStatus; storeRsp.DataSetType = DIMSE_DATASET_NULL; OFStandard::strlcpy( storeRsp.AffectedSOPClassUID, storeRequest->AffectedSOPClassUID, sizeof(storeRsp.AffectedSOPClassUID)); OFStandard::strlcpy( storeRsp.AffectedSOPInstanceUID, storeRequest->AffectedSOPInstanceUID, sizeof(storeRsp.AffectedSOPInstanceUID)); storeRsp.opts = O_STORE_AFFECTEDSOPCLASSUID | O_STORE_AFFECTEDSOPINSTANCEUID; result = DIMSE_sendMessageUsingMemoryData(&(association_->GetDcmtkAssociation()), cmdPresId, &storeResponse, NULL /* statusDetail */, NULL /* dataObject */, NULL /* progress callback TODO-GET */, NULL /* callback context */, NULL /* commandSet */); if (result.bad()) { continueSession = false; } else { OFString tempStr; CLOG(TRACE, DICOM) << "Sent C-STORE Response: " << std::endl << DIMSE_dumpMessage(tempStr, storeResponse, DIMSE_OUTGOING, NULL, cmdPresId); } } } // Handle other DIMSE command (error since other command than GET/STORE not expected) else { CLOG(WARNING, DICOM) << "Expected C-GET response or C-STORE request but received DIMSE command 0x" << std::hex << std::setfill('0') << std::setw(4) << static_cast<unsigned int>(rsp.CommandField); result = DIMSE_BADCOMMANDTYPE; continueSession = false; } // delete statusDetail; // should be NULL if not existing or added to response list // statusDetail = NULL; } /* All responses received or break signal occurred */ // return result; } DicomControlUserConnection::DicomControlUserConnection(const DicomAssociationParameters& params) : parameters_(params), association_(new DicomAssociation) { SetupPresentationContexts(); } void DicomControlUserConnection::Close() { assert(association_.get() != NULL); association_->Close(); } bool DicomControlUserConnection::Echo() { assert(association_.get() != NULL); association_->Open(parameters_); DIC_US status; DicomAssociation::CheckCondition( DIMSE_echoUser(&association_->GetDcmtkAssociation(), association_->GetDcmtkAssociation().nextMsgID++, /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), /*opt_dimse_timeout*/ parameters_.GetTimeout(), &status, NULL), parameters_, "C-ECHO"); return status == STATUS_Success; } void DicomControlUserConnection::Find(DicomFindAnswers& result, ResourceType level, const DicomMap& originalFields, bool normalize) { std::unique_ptr<ParsedDicomFile> query; if (normalize) { DicomMap fields; NormalizeFindQuery(fields, level, originalFields); query.reset(ConvertQueryFields(fields, parameters_.GetRemoteModality().GetManufacturer())); } else { query.reset(new ParsedDicomFile(originalFields, GetDefaultDicomEncoding(), false /* be strict */)); } DcmDataset* dataset = query->GetDcmtkObject().getDataset(); assert(dataset != NULL); const char* clevel = ResourceTypeToDicomQueryRetrieveLevel(level); const char* sopClass = NULL; DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, clevel); switch (level) { case ResourceType_Patient: sopClass = UID_FINDPatientRootQueryRetrieveInformationModel; break; case ResourceType_Study: sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; break; case ResourceType_Series: sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; break; case ResourceType_Instance: sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; break; default: throw OrthancException(ErrorCode_ParameterOutOfRange); } const char* universal; if (parameters_.GetRemoteModality().GetManufacturer() == ModalityManufacturer_GE) { universal = "*"; } else { universal = ""; } // Add the expected tags for this query level. // WARNING: Do not reorder or add "break" in this switch-case! switch (level) { case ResourceType_Instance: if (!dataset->tagExists(DCM_SOPInstanceUID)) { DU_putStringDOElement(dataset, DCM_SOPInstanceUID, universal); } case ResourceType_Series: if (!dataset->tagExists(DCM_SeriesInstanceUID)) { DU_putStringDOElement(dataset, DCM_SeriesInstanceUID, universal); } case ResourceType_Study: if (!dataset->tagExists(DCM_AccessionNumber)) { DU_putStringDOElement(dataset, DCM_AccessionNumber, universal); } if (!dataset->tagExists(DCM_StudyInstanceUID)) { DU_putStringDOElement(dataset, DCM_StudyInstanceUID, universal); } case ResourceType_Patient: if (!dataset->tagExists(DCM_PatientID)) { DU_putStringDOElement(dataset, DCM_PatientID, universal); } break; default: throw OrthancException(ErrorCode_ParameterOutOfRange); } assert(clevel != NULL && sopClass != NULL); FindInternal(result, dataset, sopClass, false, clevel); } void DicomControlUserConnection::Move(const std::string& targetAet, ResourceType level, const DicomMap& findResult) { DicomMap move; switch (level) { case ResourceType_Patient: TestAndCopyTag(move, findResult, DICOM_TAG_PATIENT_ID); break; case ResourceType_Study: TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID); break; case ResourceType_Series: TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID); TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID); break; case ResourceType_Instance: TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID); TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID); TestAndCopyTag(move, findResult, DICOM_TAG_SOP_INSTANCE_UID); break; default: throw OrthancException(ErrorCode_InternalError); } MoveInternal(targetAet, level, move); } void DicomControlUserConnection::Move(const std::string& targetAet, const DicomMap& findResult) { if (!findResult.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL)) { throw OrthancException(ErrorCode_InternalError); } const std::string tmp = findResult.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).GetContent(); ResourceType level = StringToResourceType(tmp.c_str()); Move(targetAet, level, findResult); } void DicomControlUserConnection::MovePatient(const std::string& targetAet, const std::string& patientId) { DicomMap query; query.SetValue(DICOM_TAG_PATIENT_ID, patientId, false); MoveInternal(targetAet, ResourceType_Patient, query); } void DicomControlUserConnection::MoveStudy(const std::string& targetAet, const std::string& studyUid) { DicomMap query; query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false); MoveInternal(targetAet, ResourceType_Study, query); } void DicomControlUserConnection::MoveSeries(const std::string& targetAet, const std::string& studyUid, const std::string& seriesUid) { DicomMap query; query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false); query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false); MoveInternal(targetAet, ResourceType_Series, query); } void DicomControlUserConnection::MoveInstance(const std::string& targetAet, const std::string& studyUid, const std::string& seriesUid, const std::string& instanceUid) { DicomMap query; query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false); query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false); query.SetValue(DICOM_TAG_SOP_INSTANCE_UID, instanceUid, false); MoveInternal(targetAet, ResourceType_Instance, query); } void DicomControlUserConnection::FindWorklist(DicomFindAnswers& result, ParsedDicomFile& query) { DcmDataset* dataset = query.GetDcmtkObject().getDataset(); const char* sopClass = UID_FINDModalityWorklistInformationModel; FindInternal(result, dataset, sopClass, true, NULL); } }