Mercurial > hg > orthanc
comparison UnitTestsSources/FromDcmtkTests.cpp @ 3820:f89eac983c9b transcoding
refactoring DicomUserConnection as DicomAssociation
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Thu, 09 Apr 2020 17:45:25 +0200 |
parents | 1237bd0bbdb2 |
children | f2488b645f5f |
comparison
equal
deleted
inserted
replaced
3819:1237bd0bbdb2 | 3820:f89eac983c9b |
---|---|
2402 { | 2402 { |
2403 TestFile("/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/1.2.840.10008.1.2.1.dcm"); | 2403 TestFile("/home/jodogne/Subversion/orthanc-tests/Database/TransferSyntaxes/1.2.840.10008.1.2.1.dcm"); |
2404 } | 2404 } |
2405 } | 2405 } |
2406 | 2406 |
2407 | |
2408 | |
2409 #ifdef _WIN32 | |
2410 /** | |
2411 * "The maximum length, in bytes, of the string returned in the buffer | |
2412 * pointed to by the name parameter is dependent on the namespace provider, | |
2413 * but this string must be 256 bytes or less. | |
2414 * http://msdn.microsoft.com/en-us/library/windows/desktop/ms738527(v=vs.85).aspx | |
2415 **/ | |
2416 # define HOST_NAME_MAX 256 | |
2417 # include <winsock.h> | |
2418 #endif | |
2419 | |
2420 | |
2421 #if !defined(HOST_NAME_MAX) && defined(_POSIX_HOST_NAME_MAX) | |
2422 /** | |
2423 * TO IMPROVE: "_POSIX_HOST_NAME_MAX is only the minimum value that | |
2424 * HOST_NAME_MAX can ever have [...] Therefore you cannot allocate an | |
2425 * array of size _POSIX_HOST_NAME_MAX, invoke gethostname() and expect | |
2426 * that the result will fit." | |
2427 * http://lists.gnu.org/archive/html/bug-gnulib/2009-08/msg00128.html | |
2428 **/ | |
2429 #define HOST_NAME_MAX _POSIX_HOST_NAME_MAX | |
2407 #endif | 2430 #endif |
2431 | |
2432 | |
2433 #include "../Core/DicomNetworking/RemoteModalityParameters.h" | |
2434 | |
2435 | |
2436 #include <dcmtk/dcmnet/diutil.h> // For dcmConnectionTimeout() | |
2437 | |
2438 | |
2439 | |
2440 namespace Orthanc | |
2441 { | |
2442 // By default, the timeout for client DICOM connections is set to 10 seconds | |
2443 static boost::mutex defaultTimeoutMutex_; | |
2444 static uint32_t defaultTimeout_ = 10; | |
2445 | |
2446 | |
2447 class DicomAssociationParameters | |
2448 { | |
2449 private: | |
2450 std::string localAet_; | |
2451 std::string remoteAet_; | |
2452 std::string remoteHost_; | |
2453 uint16_t remotePort_; | |
2454 ModalityManufacturer manufacturer_; | |
2455 DicomAssociationRole role_; | |
2456 uint32_t timeout_; | |
2457 | |
2458 void ReadDefaultTimeout() | |
2459 { | |
2460 boost::mutex::scoped_lock lock(defaultTimeoutMutex_); | |
2461 timeout_ = defaultTimeout_; | |
2462 } | |
2463 | |
2464 public: | |
2465 DicomAssociationParameters() : | |
2466 localAet_("STORESCU"), | |
2467 remoteAet_("ANY-SCP"), | |
2468 remoteHost_("127.0.0.1"), | |
2469 remotePort_(104), | |
2470 manufacturer_(ModalityManufacturer_Generic), | |
2471 role_(DicomAssociationRole_Default) | |
2472 { | |
2473 ReadDefaultTimeout(); | |
2474 } | |
2475 | |
2476 DicomAssociationParameters(const std::string& localAet, | |
2477 const RemoteModalityParameters& remote) : | |
2478 localAet_(localAet), | |
2479 remoteAet_(remote.GetApplicationEntityTitle()), | |
2480 remoteHost_(remote.GetHost()), | |
2481 remotePort_(remote.GetPortNumber()), | |
2482 manufacturer_(remote.GetManufacturer()), | |
2483 role_(DicomAssociationRole_Default), | |
2484 timeout_(defaultTimeout_) | |
2485 { | |
2486 ReadDefaultTimeout(); | |
2487 } | |
2488 | |
2489 const std::string& GetLocalApplicationEntityTitle() const | |
2490 { | |
2491 return localAet_; | |
2492 } | |
2493 | |
2494 const std::string& GetRemoteApplicationEntityTitle() const | |
2495 { | |
2496 return remoteAet_; | |
2497 } | |
2498 | |
2499 const std::string& GetRemoteHost() const | |
2500 { | |
2501 return remoteHost_; | |
2502 } | |
2503 | |
2504 uint16_t GetRemotePort() const | |
2505 { | |
2506 return remotePort_; | |
2507 } | |
2508 | |
2509 ModalityManufacturer GetRemoteManufacturer() const | |
2510 { | |
2511 return manufacturer_; | |
2512 } | |
2513 | |
2514 DicomAssociationRole GetRole() const | |
2515 { | |
2516 return role_; | |
2517 } | |
2518 | |
2519 void SetLocalApplicationEntityTitle(const std::string& aet) | |
2520 { | |
2521 localAet_ = aet; | |
2522 } | |
2523 | |
2524 void SetRemoteApplicationEntityTitle(const std::string& aet) | |
2525 { | |
2526 remoteAet_ = aet; | |
2527 } | |
2528 | |
2529 void SetRemoteHost(const std::string& host) | |
2530 { | |
2531 if (host.size() > HOST_NAME_MAX - 10) | |
2532 { | |
2533 throw OrthancException(ErrorCode_ParameterOutOfRange, | |
2534 "Invalid host name (too long): " + host); | |
2535 } | |
2536 | |
2537 remoteHost_ = host; | |
2538 } | |
2539 | |
2540 void SetRemotePort(uint16_t port) | |
2541 { | |
2542 remotePort_ = port; | |
2543 } | |
2544 | |
2545 void SetRemoteManufacturer(ModalityManufacturer manufacturer) | |
2546 { | |
2547 manufacturer_ = manufacturer; | |
2548 } | |
2549 | |
2550 void SetRole(DicomAssociationRole role) | |
2551 { | |
2552 role_ = role; | |
2553 } | |
2554 | |
2555 void SetRemoteModality(const RemoteModalityParameters& parameters) | |
2556 { | |
2557 SetRemoteApplicationEntityTitle(parameters.GetApplicationEntityTitle()); | |
2558 SetRemoteHost(parameters.GetHost()); | |
2559 SetRemotePort(parameters.GetPortNumber()); | |
2560 SetRemoteManufacturer(parameters.GetManufacturer()); | |
2561 } | |
2562 | |
2563 bool IsEqual(const DicomAssociationParameters& other) const | |
2564 { | |
2565 return (localAet_ == other.localAet_ && | |
2566 remoteAet_ == other.remoteAet_ && | |
2567 remoteHost_ == other.remoteHost_ && | |
2568 remotePort_ == other.remotePort_ && | |
2569 manufacturer_ == other.manufacturer_ && | |
2570 role_ == other.role_); | |
2571 } | |
2572 | |
2573 void SetTimeout(uint32_t seconds) | |
2574 { | |
2575 timeout_ = seconds; | |
2576 } | |
2577 | |
2578 uint32_t GetTimeout() const | |
2579 { | |
2580 return timeout_; | |
2581 } | |
2582 | |
2583 bool HasTimeout() const | |
2584 { | |
2585 return timeout_ != 0; | |
2586 } | |
2587 | |
2588 static void SetDefaultTimeout(uint32_t seconds) | |
2589 { | |
2590 LOG(INFO) << "Default timeout for DICOM connections if Orthanc acts as SCU (client): " | |
2591 << seconds << " seconds (0 = no timeout)"; | |
2592 | |
2593 { | |
2594 boost::mutex::scoped_lock lock(defaultTimeoutMutex_); | |
2595 defaultTimeout_ = seconds; | |
2596 } | |
2597 } | |
2598 | |
2599 void CheckCondition(const OFCondition& cond, | |
2600 const std::string& command) const | |
2601 { | |
2602 if (cond.bad()) | |
2603 { | |
2604 // Reformat the error message from DCMTK by turning multiline | |
2605 // errors into a single line | |
2606 | |
2607 std::string s(cond.text()); | |
2608 std::string info; | |
2609 info.reserve(s.size()); | |
2610 | |
2611 bool isMultiline = false; | |
2612 for (size_t i = 0; i < s.size(); i++) | |
2613 { | |
2614 if (s[i] == '\r') | |
2615 { | |
2616 // Ignore | |
2617 } | |
2618 else if (s[i] == '\n') | |
2619 { | |
2620 if (isMultiline) | |
2621 { | |
2622 info += "; "; | |
2623 } | |
2624 else | |
2625 { | |
2626 info += " ("; | |
2627 isMultiline = true; | |
2628 } | |
2629 } | |
2630 else | |
2631 { | |
2632 info.push_back(s[i]); | |
2633 } | |
2634 } | |
2635 | |
2636 if (isMultiline) | |
2637 { | |
2638 info += ")"; | |
2639 } | |
2640 | |
2641 throw OrthancException(ErrorCode_NetworkProtocol, | |
2642 "DicomUserConnection - " + command + " to AET \"" + | |
2643 GetRemoteApplicationEntityTitle() + "\": " + info); | |
2644 } | |
2645 } | |
2646 }; | |
2647 | |
2648 | |
2649 class DicomAssociation : public boost::noncopyable | |
2650 { | |
2651 private: | |
2652 // This is the maximum number of presentation context IDs (the | |
2653 // number of odd integers between 1 and 255) | |
2654 // http://dicom.nema.org/medical/dicom/2019e/output/chtml/part08/sect_9.3.2.2.html | |
2655 static const size_t MAX_PROPOSED_PRESENTATIONS = 128; | |
2656 | |
2657 struct ProposedPresentationContext | |
2658 { | |
2659 std::string sopClassUid_; | |
2660 std::set<DicomTransferSyntax> transferSyntaxes_; | |
2661 }; | |
2662 | |
2663 typedef std::map<std::string, std::map<DicomTransferSyntax, uint8_t> > AcceptedPresentationContexts; | |
2664 | |
2665 bool isOpen_; | |
2666 std::vector<ProposedPresentationContext> proposed_; | |
2667 AcceptedPresentationContexts accepted_; | |
2668 T_ASC_Network* net_; | |
2669 T_ASC_Parameters* params_; | |
2670 T_ASC_Association* assoc_; | |
2671 | |
2672 void Initialize() | |
2673 { | |
2674 isOpen_ = false; | |
2675 net_ = NULL; | |
2676 params_ = NULL; | |
2677 assoc_ = NULL; | |
2678 | |
2679 // Must be after "isOpen_ = false" | |
2680 ClearPresentationContexts(); | |
2681 } | |
2682 | |
2683 void CheckConnecting(const DicomAssociationParameters& parameters, | |
2684 const OFCondition& cond) | |
2685 { | |
2686 try | |
2687 { | |
2688 parameters.CheckCondition(cond, "connecting"); | |
2689 } | |
2690 catch (OrthancException&) | |
2691 { | |
2692 CloseInternal(); | |
2693 throw; | |
2694 } | |
2695 } | |
2696 | |
2697 void CloseInternal() | |
2698 { | |
2699 if (assoc_ != NULL) | |
2700 { | |
2701 ASC_releaseAssociation(assoc_); | |
2702 ASC_destroyAssociation(&assoc_); | |
2703 assoc_ = NULL; | |
2704 params_ = NULL; | |
2705 } | |
2706 else | |
2707 { | |
2708 if (params_ != NULL) | |
2709 { | |
2710 ASC_destroyAssociationParameters(¶ms_); | |
2711 params_ = NULL; | |
2712 } | |
2713 } | |
2714 | |
2715 if (net_ != NULL) | |
2716 { | |
2717 ASC_dropNetwork(&net_); | |
2718 net_ = NULL; | |
2719 } | |
2720 | |
2721 accepted_.clear(); | |
2722 isOpen_ = false; | |
2723 } | |
2724 | |
2725 void AddAccepted(const std::string& sopClassUid, | |
2726 DicomTransferSyntax syntax, | |
2727 uint8_t presentationContextId) | |
2728 { | |
2729 AcceptedPresentationContexts::iterator found = accepted_.find(sopClassUid); | |
2730 | |
2731 if (found == accepted_.end()) | |
2732 { | |
2733 std::map<DicomTransferSyntax, uint8_t> syntaxes; | |
2734 syntaxes[syntax] = presentationContextId; | |
2735 accepted_[sopClassUid] = syntaxes; | |
2736 } | |
2737 else | |
2738 { | |
2739 if (found->second.find(syntax) != found->second.end()) | |
2740 { | |
2741 LOG(WARNING) << "The same transfer syntax (" | |
2742 << GetTransferSyntaxUid(syntax) | |
2743 << ") was accepted twice for the same SOP class UID (" | |
2744 << sopClassUid << ")"; | |
2745 } | |
2746 else | |
2747 { | |
2748 found->second[syntax] = presentationContextId; | |
2749 } | |
2750 } | |
2751 } | |
2752 | |
2753 public: | |
2754 DicomAssociation() | |
2755 { | |
2756 Initialize(); | |
2757 } | |
2758 | |
2759 ~DicomAssociation() | |
2760 { | |
2761 try | |
2762 { | |
2763 Close(); | |
2764 } | |
2765 catch (OrthancException&) | |
2766 { | |
2767 // Don't throw exception in destructors | |
2768 } | |
2769 } | |
2770 | |
2771 bool IsOpen() const | |
2772 { | |
2773 return isOpen_; | |
2774 } | |
2775 | |
2776 void ClearPresentationContexts() | |
2777 { | |
2778 Close(); | |
2779 proposed_.clear(); | |
2780 proposed_.reserve(MAX_PROPOSED_PRESENTATIONS); | |
2781 } | |
2782 | |
2783 void Open(const DicomAssociationParameters& parameters) | |
2784 { | |
2785 if (isOpen_) | |
2786 { | |
2787 return; // Already open | |
2788 } | |
2789 | |
2790 // Timeout used during association negociation and ASC_releaseAssociation() | |
2791 uint32_t acseTimeout = parameters.GetTimeout(); | |
2792 if (acseTimeout == 0) | |
2793 { | |
2794 /** | |
2795 * Timeout is disabled. Global timeout (seconds) for | |
2796 * connecting to remote hosts. Default value is -1 which | |
2797 * selects infinite timeout, i.e. blocking connect(). | |
2798 **/ | |
2799 dcmConnectionTimeout.set(-1); | |
2800 acseTimeout = 10; | |
2801 } | |
2802 else | |
2803 { | |
2804 dcmConnectionTimeout.set(acseTimeout); | |
2805 } | |
2806 | |
2807 T_ASC_SC_ROLE dcmtkRole; | |
2808 switch (parameters.GetRole()) | |
2809 { | |
2810 case DicomAssociationRole_Default: | |
2811 dcmtkRole = ASC_SC_ROLE_DEFAULT; | |
2812 break; | |
2813 | |
2814 case DicomAssociationRole_Scu: | |
2815 dcmtkRole = ASC_SC_ROLE_SCU; | |
2816 break; | |
2817 | |
2818 case DicomAssociationRole_Scp: | |
2819 dcmtkRole = ASC_SC_ROLE_SCP; | |
2820 break; | |
2821 | |
2822 default: | |
2823 throw OrthancException(ErrorCode_ParameterOutOfRange); | |
2824 } | |
2825 | |
2826 assert(net_ == NULL && | |
2827 params_ == NULL && | |
2828 assoc_ == NULL); | |
2829 | |
2830 if (proposed_.empty()) | |
2831 { | |
2832 throw OrthancException(ErrorCode_BadSequenceOfCalls, | |
2833 "No presentation context was proposed"); | |
2834 } | |
2835 | |
2836 LOG(INFO) << "Opening a DICOM SCU connection from AET \"" | |
2837 << parameters.GetLocalApplicationEntityTitle() | |
2838 << "\" to AET \"" << parameters.GetRemoteApplicationEntityTitle() | |
2839 << "\" on host " << parameters.GetRemoteHost() | |
2840 << ":" << parameters.GetRemotePort() | |
2841 << " (manufacturer: " << EnumerationToString(parameters.GetRemoteManufacturer()) << ")"; | |
2842 | |
2843 CheckConnecting(parameters, ASC_initializeNetwork(NET_REQUESTOR, 0, /*opt_acse_timeout*/ acseTimeout, &net_)); | |
2844 CheckConnecting(parameters, ASC_createAssociationParameters(¶ms_, /*opt_maxReceivePDULength*/ ASC_DEFAULTMAXPDU)); | |
2845 | |
2846 // Set this application's title and the called application's title in the params | |
2847 CheckConnecting(parameters, ASC_setAPTitles( | |
2848 params_, parameters.GetLocalApplicationEntityTitle().c_str(), | |
2849 parameters.GetRemoteApplicationEntityTitle().c_str(), NULL)); | |
2850 | |
2851 // Set the network addresses of the local and remote entities | |
2852 char localHost[HOST_NAME_MAX]; | |
2853 gethostname(localHost, HOST_NAME_MAX - 1); | |
2854 | |
2855 char remoteHostAndPort[HOST_NAME_MAX]; | |
2856 | |
2857 #ifdef _MSC_VER | |
2858 _snprintf | |
2859 #else | |
2860 snprintf | |
2861 #endif | |
2862 (remoteHostAndPort, HOST_NAME_MAX - 1, "%s:%d", | |
2863 parameters.GetRemoteHost().c_str(), parameters.GetRemotePort()); | |
2864 | |
2865 CheckConnecting(parameters, ASC_setPresentationAddresses(params_, localHost, remoteHostAndPort)); | |
2866 | |
2867 // Set various options | |
2868 CheckConnecting(parameters, ASC_setTransportLayerType(params_, /*opt_secureConnection*/ false)); | |
2869 | |
2870 // Setup the list of proposed presentation contexts | |
2871 unsigned int presentationContextId = 1; | |
2872 for (size_t i = 0; i < proposed_.size(); i++) | |
2873 { | |
2874 assert(presentationContextId <= 255); | |
2875 const char* sopClassUid = proposed_[i].sopClassUid_.c_str(); | |
2876 | |
2877 const std::set<DicomTransferSyntax>& source = proposed_[i].transferSyntaxes_; | |
2878 | |
2879 std::vector<const char*> transferSyntaxes; | |
2880 transferSyntaxes.reserve(source.size()); | |
2881 | |
2882 for (std::set<DicomTransferSyntax>::const_iterator | |
2883 it = source.begin(); it != source.end(); ++it) | |
2884 { | |
2885 transferSyntaxes.push_back(GetTransferSyntaxUid(*it)); | |
2886 } | |
2887 | |
2888 assert(!transferSyntaxes.empty()); | |
2889 CheckConnecting(parameters, ASC_addPresentationContext( | |
2890 params_, presentationContextId, sopClassUid, | |
2891 &transferSyntaxes[0], transferSyntaxes.size(), dcmtkRole)); | |
2892 | |
2893 presentationContextId += 2; | |
2894 } | |
2895 | |
2896 // Do the association | |
2897 CheckConnecting(parameters, ASC_requestAssociation(net_, params_, &assoc_)); | |
2898 isOpen_ = true; | |
2899 | |
2900 // Inspect the accepted transfer syntaxes | |
2901 LST_HEAD **l = ¶ms_->DULparams.acceptedPresentationContext; | |
2902 if (*l != NULL) | |
2903 { | |
2904 DUL_PRESENTATIONCONTEXT* pc = (DUL_PRESENTATIONCONTEXT*) LST_Head(l); | |
2905 LST_Position(l, (LST_NODE*)pc); | |
2906 while (pc) | |
2907 { | |
2908 if (pc->result == ASC_P_ACCEPTANCE) | |
2909 { | |
2910 DicomTransferSyntax transferSyntax; | |
2911 if (LookupTransferSyntax(transferSyntax, pc->acceptedTransferSyntax)) | |
2912 { | |
2913 AddAccepted(pc->abstractSyntax, transferSyntax, pc->presentationContextID); | |
2914 } | |
2915 else | |
2916 { | |
2917 LOG(WARNING) << "Unknown transfer syntax received from AET \"" | |
2918 << parameters.GetRemoteApplicationEntityTitle() | |
2919 << "\": " << pc->acceptedTransferSyntax; | |
2920 } | |
2921 } | |
2922 | |
2923 pc = (DUL_PRESENTATIONCONTEXT*) LST_Next(l); | |
2924 } | |
2925 } | |
2926 | |
2927 if (accepted_.empty()) | |
2928 { | |
2929 throw OrthancException(ErrorCode_NoPresentationContext, | |
2930 "Unable to negotiate a presentation context with AET \"" + | |
2931 parameters.GetRemoteApplicationEntityTitle() + "\""); | |
2932 } | |
2933 } | |
2934 | |
2935 void Close() | |
2936 { | |
2937 if (isOpen_) | |
2938 { | |
2939 CloseInternal(); | |
2940 } | |
2941 } | |
2942 | |
2943 bool LookupAcceptedPresentationContext(std::map<DicomTransferSyntax, uint8_t>& target, | |
2944 const std::string& sopClassUid) const | |
2945 { | |
2946 if (!IsOpen()) | |
2947 { | |
2948 throw OrthancException(ErrorCode_BadSequenceOfCalls, "Connection not opened"); | |
2949 } | |
2950 | |
2951 AcceptedPresentationContexts::const_iterator found = accepted_.find(sopClassUid); | |
2952 | |
2953 if (found == accepted_.end()) | |
2954 { | |
2955 return false; | |
2956 } | |
2957 else | |
2958 { | |
2959 target = found->second; | |
2960 return true; | |
2961 } | |
2962 } | |
2963 | |
2964 void ProposeGenericPresentationContext(const std::string& sopClassUid) | |
2965 { | |
2966 std::set<DicomTransferSyntax> ts; | |
2967 ts.insert(DicomTransferSyntax_LittleEndianImplicit); | |
2968 ts.insert(DicomTransferSyntax_LittleEndianExplicit); | |
2969 ts.insert(DicomTransferSyntax_BigEndianExplicit); | |
2970 ProposePresentationContext(sopClassUid, ts); | |
2971 } | |
2972 | |
2973 void ProposePresentationContext(const std::string& sopClassUid, | |
2974 DicomTransferSyntax transferSyntax) | |
2975 { | |
2976 std::set<DicomTransferSyntax> ts; | |
2977 ts.insert(transferSyntax); | |
2978 ProposePresentationContext(sopClassUid, ts); | |
2979 } | |
2980 | |
2981 void ProposePresentationContext(const std::string& sopClassUid, | |
2982 const std::set<DicomTransferSyntax>& transferSyntaxes) | |
2983 { | |
2984 if (transferSyntaxes.empty()) | |
2985 { | |
2986 throw OrthancException(ErrorCode_ParameterOutOfRange, | |
2987 "No transfer syntax provided"); | |
2988 } | |
2989 | |
2990 if (proposed_.size() >= MAX_PROPOSED_PRESENTATIONS) | |
2991 { | |
2992 throw OrthancException(ErrorCode_ParameterOutOfRange, | |
2993 "Too many proposed presentation contexts"); | |
2994 } | |
2995 | |
2996 if (IsOpen()) | |
2997 { | |
2998 Close(); | |
2999 } | |
3000 | |
3001 ProposedPresentationContext context; | |
3002 context.sopClassUid_ = sopClassUid; | |
3003 context.transferSyntaxes_ = transferSyntaxes; | |
3004 | |
3005 proposed_.push_back(context); | |
3006 } | |
3007 | |
3008 T_ASC_Association& GetDcmtkAssociation() const | |
3009 { | |
3010 if (isOpen_) | |
3011 { | |
3012 assert(assoc_ != NULL); | |
3013 return *assoc_; | |
3014 } | |
3015 else | |
3016 { | |
3017 throw OrthancException(ErrorCode_BadSequenceOfCalls, | |
3018 "The connection is not open"); | |
3019 } | |
3020 } | |
3021 | |
3022 T_ASC_Network& GetDcmtkNetwork() const | |
3023 { | |
3024 if (isOpen_) | |
3025 { | |
3026 assert(net_ != NULL); | |
3027 return *net_; | |
3028 } | |
3029 else | |
3030 { | |
3031 throw OrthancException(ErrorCode_BadSequenceOfCalls, | |
3032 "The connection is not open"); | |
3033 } | |
3034 } | |
3035 }; | |
3036 | |
3037 | |
3038 | |
3039 static void TestAndCopyTag(DicomMap& result, | |
3040 const DicomMap& source, | |
3041 const DicomTag& tag) | |
3042 { | |
3043 if (!source.HasTag(tag)) | |
3044 { | |
3045 throw OrthancException(ErrorCode_BadRequest); | |
3046 } | |
3047 else | |
3048 { | |
3049 result.SetValue(tag, source.GetValue(tag)); | |
3050 } | |
3051 } | |
3052 | |
3053 | |
3054 namespace | |
3055 { | |
3056 struct FindPayload | |
3057 { | |
3058 DicomFindAnswers* answers; | |
3059 const char* level; | |
3060 bool isWorklist; | |
3061 }; | |
3062 } | |
3063 | |
3064 | |
3065 static void FindCallback( | |
3066 /* in */ | |
3067 void *callbackData, | |
3068 T_DIMSE_C_FindRQ *request, /* original find request */ | |
3069 int responseCount, | |
3070 T_DIMSE_C_FindRSP *response, /* pending response received */ | |
3071 DcmDataset *responseIdentifiers /* pending response identifiers */ | |
3072 ) | |
3073 { | |
3074 FindPayload& payload = *reinterpret_cast<FindPayload*>(callbackData); | |
3075 | |
3076 if (responseIdentifiers != NULL) | |
3077 { | |
3078 if (payload.isWorklist) | |
3079 { | |
3080 ParsedDicomFile answer(*responseIdentifiers); | |
3081 payload.answers->Add(answer); | |
3082 } | |
3083 else | |
3084 { | |
3085 DicomMap m; | |
3086 FromDcmtkBridge::ExtractDicomSummary(m, *responseIdentifiers); | |
3087 | |
3088 if (!m.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL)) | |
3089 { | |
3090 m.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, payload.level, false); | |
3091 } | |
3092 | |
3093 payload.answers->Add(m); | |
3094 } | |
3095 } | |
3096 } | |
3097 | |
3098 | |
3099 static void NormalizeFindQuery(DicomMap& fixedQuery, | |
3100 ResourceType level, | |
3101 const DicomMap& fields) | |
3102 { | |
3103 std::set<DicomTag> allowedTags; | |
3104 | |
3105 // WARNING: Do not add "break" or reorder items in this switch-case! | |
3106 switch (level) | |
3107 { | |
3108 case ResourceType_Instance: | |
3109 DicomTag::AddTagsForModule(allowedTags, DicomModule_Instance); | |
3110 | |
3111 case ResourceType_Series: | |
3112 DicomTag::AddTagsForModule(allowedTags, DicomModule_Series); | |
3113 | |
3114 case ResourceType_Study: | |
3115 DicomTag::AddTagsForModule(allowedTags, DicomModule_Study); | |
3116 | |
3117 case ResourceType_Patient: | |
3118 DicomTag::AddTagsForModule(allowedTags, DicomModule_Patient); | |
3119 break; | |
3120 | |
3121 default: | |
3122 throw OrthancException(ErrorCode_InternalError); | |
3123 } | |
3124 | |
3125 switch (level) | |
3126 { | |
3127 case ResourceType_Patient: | |
3128 allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_STUDIES); | |
3129 allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_SERIES); | |
3130 allowedTags.insert(DICOM_TAG_NUMBER_OF_PATIENT_RELATED_INSTANCES); | |
3131 break; | |
3132 | |
3133 case ResourceType_Study: | |
3134 allowedTags.insert(DICOM_TAG_MODALITIES_IN_STUDY); | |
3135 allowedTags.insert(DICOM_TAG_NUMBER_OF_STUDY_RELATED_SERIES); | |
3136 allowedTags.insert(DICOM_TAG_NUMBER_OF_STUDY_RELATED_INSTANCES); | |
3137 allowedTags.insert(DICOM_TAG_SOP_CLASSES_IN_STUDY); | |
3138 break; | |
3139 | |
3140 case ResourceType_Series: | |
3141 allowedTags.insert(DICOM_TAG_NUMBER_OF_SERIES_RELATED_INSTANCES); | |
3142 break; | |
3143 | |
3144 default: | |
3145 break; | |
3146 } | |
3147 | |
3148 allowedTags.insert(DICOM_TAG_SPECIFIC_CHARACTER_SET); | |
3149 | |
3150 DicomArray query(fields); | |
3151 for (size_t i = 0; i < query.GetSize(); i++) | |
3152 { | |
3153 const DicomTag& tag = query.GetElement(i).GetTag(); | |
3154 if (allowedTags.find(tag) == allowedTags.end()) | |
3155 { | |
3156 LOG(WARNING) << "Tag not allowed for this C-Find level, will be ignored: " << tag; | |
3157 } | |
3158 else | |
3159 { | |
3160 fixedQuery.SetValue(tag, query.GetElement(i).GetValue()); | |
3161 } | |
3162 } | |
3163 } | |
3164 | |
3165 | |
3166 | |
3167 static ParsedDicomFile* ConvertQueryFields(const DicomMap& fields, | |
3168 ModalityManufacturer manufacturer) | |
3169 { | |
3170 // Fix outgoing C-Find requests issue for Syngo.Via and its | |
3171 // solution was reported by Emsy Chan by private mail on | |
3172 // 2015-06-17. According to Robert van Ommen (2015-11-30), the | |
3173 // same fix is required for Agfa Impax. This was generalized for | |
3174 // generic manufacturer since it seems to affect PhilipsADW, | |
3175 // GEWAServer as well: | |
3176 // https://bitbucket.org/sjodogne/orthanc/issues/31/ | |
3177 | |
3178 switch (manufacturer) | |
3179 { | |
3180 case ModalityManufacturer_GenericNoWildcardInDates: | |
3181 case ModalityManufacturer_GenericNoUniversalWildcard: | |
3182 { | |
3183 std::unique_ptr<DicomMap> fix(fields.Clone()); | |
3184 | |
3185 std::set<DicomTag> tags; | |
3186 fix->GetTags(tags); | |
3187 | |
3188 for (std::set<DicomTag>::const_iterator it = tags.begin(); it != tags.end(); ++it) | |
3189 { | |
3190 // Replace a "*" wildcard query by an empty query ("") for | |
3191 // "date" or "all" value representations depending on the | |
3192 // type of manufacturer. | |
3193 if (manufacturer == ModalityManufacturer_GenericNoUniversalWildcard || | |
3194 (manufacturer == ModalityManufacturer_GenericNoWildcardInDates && | |
3195 FromDcmtkBridge::LookupValueRepresentation(*it) == ValueRepresentation_Date)) | |
3196 { | |
3197 const DicomValue* value = fix->TestAndGetValue(*it); | |
3198 | |
3199 if (value != NULL && | |
3200 !value->IsNull() && | |
3201 value->GetContent() == "*") | |
3202 { | |
3203 fix->SetValue(*it, "", false); | |
3204 } | |
3205 } | |
3206 } | |
3207 | |
3208 return new ParsedDicomFile(*fix, GetDefaultDicomEncoding(), false /* be strict */); | |
3209 } | |
3210 | |
3211 default: | |
3212 return new ParsedDicomFile(fields, GetDefaultDicomEncoding(), false /* be strict */); | |
3213 } | |
3214 } | |
3215 | |
3216 | |
3217 | |
3218 class DicomControlUserConnection : public boost::noncopyable | |
3219 { | |
3220 private: | |
3221 DicomAssociationParameters parameters_; | |
3222 DicomAssociation association_; | |
3223 | |
3224 void SetupPresentationContexts() | |
3225 { | |
3226 association_.ProposeGenericPresentationContext(UID_VerificationSOPClass); | |
3227 association_.ProposeGenericPresentationContext(UID_FINDPatientRootQueryRetrieveInformationModel); | |
3228 association_.ProposeGenericPresentationContext(UID_FINDStudyRootQueryRetrieveInformationModel); | |
3229 association_.ProposeGenericPresentationContext(UID_MOVEStudyRootQueryRetrieveInformationModel); | |
3230 association_.ProposeGenericPresentationContext(UID_FINDModalityWorklistInformationModel); | |
3231 } | |
3232 | |
3233 void FindInternal(DicomFindAnswers& answers, | |
3234 DcmDataset* dataset, | |
3235 const char* sopClass, | |
3236 bool isWorklist, | |
3237 const char* level) | |
3238 { | |
3239 assert(isWorklist ^ (level != NULL)); | |
3240 | |
3241 association_.Open(parameters_); | |
3242 | |
3243 FindPayload payload; | |
3244 payload.answers = &answers; | |
3245 payload.level = level; | |
3246 payload.isWorklist = isWorklist; | |
3247 | |
3248 // Figure out which of the accepted presentation contexts should be used | |
3249 int presID = ASC_findAcceptedPresentationContextID( | |
3250 &association_.GetDcmtkAssociation(), sopClass); | |
3251 if (presID == 0) | |
3252 { | |
3253 throw OrthancException(ErrorCode_DicomFindUnavailable, | |
3254 "Remote AET is " + parameters_.GetRemoteApplicationEntityTitle()); | |
3255 } | |
3256 | |
3257 T_DIMSE_C_FindRQ request; | |
3258 memset(&request, 0, sizeof(request)); | |
3259 request.MessageID = association_.GetDcmtkAssociation().nextMsgID++; | |
3260 strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN); | |
3261 request.Priority = DIMSE_PRIORITY_MEDIUM; | |
3262 request.DataSetType = DIMSE_DATASET_PRESENT; | |
3263 | |
3264 T_DIMSE_C_FindRSP response; | |
3265 DcmDataset* statusDetail = NULL; | |
3266 | |
3267 #if DCMTK_VERSION_NUMBER >= 364 | |
3268 int responseCount; | |
3269 #endif | |
3270 | |
3271 OFCondition cond = DIMSE_findUser( | |
3272 &association_.GetDcmtkAssociation(), presID, &request, dataset, | |
3273 #if DCMTK_VERSION_NUMBER >= 364 | |
3274 responseCount, | |
3275 #endif | |
3276 FindCallback, &payload, | |
3277 /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), | |
3278 /*opt_dimse_timeout*/ parameters_.GetTimeout(), | |
3279 &response, &statusDetail); | |
3280 | |
3281 if (statusDetail) | |
3282 { | |
3283 delete statusDetail; | |
3284 } | |
3285 | |
3286 parameters_.CheckCondition(cond, "C-FIND"); | |
3287 | |
3288 | |
3289 /** | |
3290 * New in Orthanc 1.6.0: Deal with failures during C-FIND. | |
3291 * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.html#table_C.4-1 | |
3292 **/ | |
3293 | |
3294 if (response.DimseStatus != 0x0000 && // Success | |
3295 response.DimseStatus != 0xFF00 && // Pending - Matches are continuing | |
3296 response.DimseStatus != 0xFF01) // Pending - Matches are continuing | |
3297 { | |
3298 char buf[16]; | |
3299 sprintf(buf, "%04X", response.DimseStatus); | |
3300 | |
3301 if (response.DimseStatus == STATUS_FIND_Failed_UnableToProcess) | |
3302 { | |
3303 throw OrthancException(ErrorCode_NetworkProtocol, | |
3304 HttpStatus_422_UnprocessableEntity, | |
3305 "C-FIND SCU to AET \"" + | |
3306 parameters_.GetRemoteApplicationEntityTitle() + | |
3307 "\" has failed with DIMSE status 0x" + buf + | |
3308 " (unable to process - invalid query ?)"); | |
3309 } | |
3310 else | |
3311 { | |
3312 throw OrthancException(ErrorCode_NetworkProtocol, "C-FIND SCU to AET \"" + | |
3313 parameters_.GetRemoteApplicationEntityTitle() + | |
3314 "\" has failed with DIMSE status 0x" + buf); | |
3315 } | |
3316 } | |
3317 } | |
3318 | |
3319 void MoveInternal(const std::string& targetAet, | |
3320 ResourceType level, | |
3321 const DicomMap& fields) | |
3322 { | |
3323 association_.Open(parameters_); | |
3324 | |
3325 std::unique_ptr<ParsedDicomFile> query( | |
3326 ConvertQueryFields(fields, parameters_.GetRemoteManufacturer())); | |
3327 DcmDataset* dataset = query->GetDcmtkObject().getDataset(); | |
3328 | |
3329 const char* sopClass = UID_MOVEStudyRootQueryRetrieveInformationModel; | |
3330 switch (level) | |
3331 { | |
3332 case ResourceType_Patient: | |
3333 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "PATIENT"); | |
3334 break; | |
3335 | |
3336 case ResourceType_Study: | |
3337 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "STUDY"); | |
3338 break; | |
3339 | |
3340 case ResourceType_Series: | |
3341 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "SERIES"); | |
3342 break; | |
3343 | |
3344 case ResourceType_Instance: | |
3345 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "IMAGE"); | |
3346 break; | |
3347 | |
3348 default: | |
3349 throw OrthancException(ErrorCode_ParameterOutOfRange); | |
3350 } | |
3351 | |
3352 // Figure out which of the accepted presentation contexts should be used | |
3353 int presID = ASC_findAcceptedPresentationContextID(&association_.GetDcmtkAssociation(), sopClass); | |
3354 if (presID == 0) | |
3355 { | |
3356 throw OrthancException(ErrorCode_DicomMoveUnavailable, | |
3357 "Remote AET is " + parameters_.GetRemoteApplicationEntityTitle()); | |
3358 } | |
3359 | |
3360 T_DIMSE_C_MoveRQ request; | |
3361 memset(&request, 0, sizeof(request)); | |
3362 request.MessageID = association_.GetDcmtkAssociation().nextMsgID++; | |
3363 strncpy(request.AffectedSOPClassUID, sopClass, DIC_UI_LEN); | |
3364 request.Priority = DIMSE_PRIORITY_MEDIUM; | |
3365 request.DataSetType = DIMSE_DATASET_PRESENT; | |
3366 strncpy(request.MoveDestination, targetAet.c_str(), DIC_AE_LEN); | |
3367 | |
3368 T_DIMSE_C_MoveRSP response; | |
3369 DcmDataset* statusDetail = NULL; | |
3370 DcmDataset* responseIdentifiers = NULL; | |
3371 OFCondition cond = DIMSE_moveUser( | |
3372 &association_.GetDcmtkAssociation(), presID, &request, dataset, NULL, NULL, | |
3373 /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), | |
3374 /*opt_dimse_timeout*/ parameters_.GetTimeout(), | |
3375 &association_.GetDcmtkNetwork(), NULL, NULL, | |
3376 &response, &statusDetail, &responseIdentifiers); | |
3377 | |
3378 if (statusDetail) | |
3379 { | |
3380 delete statusDetail; | |
3381 } | |
3382 | |
3383 if (responseIdentifiers) | |
3384 { | |
3385 delete responseIdentifiers; | |
3386 } | |
3387 | |
3388 parameters_.CheckCondition(cond, "C-MOVE"); | |
3389 | |
3390 | |
3391 /** | |
3392 * New in Orthanc 1.6.0: Deal with failures during C-MOVE. | |
3393 * http://dicom.nema.org/medical/dicom/current/output/chtml/part04/sect_C.4.2.html#table_C.4-2 | |
3394 **/ | |
3395 | |
3396 if (response.DimseStatus != 0x0000 && // Success | |
3397 response.DimseStatus != 0xFF00) // Pending - Sub-operations are continuing | |
3398 { | |
3399 char buf[16]; | |
3400 sprintf(buf, "%04X", response.DimseStatus); | |
3401 | |
3402 if (response.DimseStatus == STATUS_MOVE_Failed_UnableToProcess) | |
3403 { | |
3404 throw OrthancException(ErrorCode_NetworkProtocol, | |
3405 HttpStatus_422_UnprocessableEntity, | |
3406 "C-MOVE SCU to AET \"" + | |
3407 parameters_.GetRemoteApplicationEntityTitle() + | |
3408 "\" has failed with DIMSE status 0x" + buf + | |
3409 " (unable to process - resource not found ?)"); | |
3410 } | |
3411 else | |
3412 { | |
3413 throw OrthancException(ErrorCode_NetworkProtocol, "C-MOVE SCU to AET \"" + | |
3414 parameters_.GetRemoteApplicationEntityTitle() + | |
3415 "\" has failed with DIMSE status 0x" + buf); | |
3416 } | |
3417 } | |
3418 } | |
3419 | |
3420 public: | |
3421 DicomControlUserConnection() | |
3422 { | |
3423 SetupPresentationContexts(); | |
3424 } | |
3425 | |
3426 DicomControlUserConnection(const DicomAssociationParameters& params) : | |
3427 parameters_(params) | |
3428 { | |
3429 SetupPresentationContexts(); | |
3430 } | |
3431 | |
3432 void SetParameters(const DicomAssociationParameters& params) | |
3433 { | |
3434 if (!parameters_.IsEqual(params)) | |
3435 { | |
3436 association_.Close(); | |
3437 parameters_ = params; | |
3438 } | |
3439 } | |
3440 | |
3441 const DicomAssociationParameters& GetParameters() const | |
3442 { | |
3443 return parameters_; | |
3444 } | |
3445 | |
3446 bool Echo() | |
3447 { | |
3448 association_.Open(parameters_); | |
3449 | |
3450 DIC_US status; | |
3451 parameters_.CheckCondition( | |
3452 DIMSE_echoUser(&association_.GetDcmtkAssociation(), | |
3453 association_.GetDcmtkAssociation().nextMsgID++, | |
3454 /*opt_blockMode*/ (parameters_.HasTimeout() ? DIMSE_NONBLOCKING : DIMSE_BLOCKING), | |
3455 /*opt_dimse_timeout*/ parameters_.GetTimeout(), | |
3456 &status, NULL), | |
3457 "C-ECHO"); | |
3458 | |
3459 return status == STATUS_Success; | |
3460 } | |
3461 | |
3462 | |
3463 void Find(DicomFindAnswers& result, | |
3464 ResourceType level, | |
3465 const DicomMap& originalFields, | |
3466 bool normalize) | |
3467 { | |
3468 std::unique_ptr<ParsedDicomFile> query; | |
3469 | |
3470 if (normalize) | |
3471 { | |
3472 DicomMap fields; | |
3473 NormalizeFindQuery(fields, level, originalFields); | |
3474 query.reset(ConvertQueryFields(fields, parameters_.GetRemoteManufacturer())); | |
3475 } | |
3476 else | |
3477 { | |
3478 query.reset(new ParsedDicomFile(originalFields, | |
3479 GetDefaultDicomEncoding(), | |
3480 false /* be strict */)); | |
3481 } | |
3482 | |
3483 DcmDataset* dataset = query->GetDcmtkObject().getDataset(); | |
3484 | |
3485 const char* clevel = NULL; | |
3486 const char* sopClass = NULL; | |
3487 | |
3488 switch (level) | |
3489 { | |
3490 case ResourceType_Patient: | |
3491 clevel = "PATIENT"; | |
3492 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "PATIENT"); | |
3493 sopClass = UID_FINDPatientRootQueryRetrieveInformationModel; | |
3494 break; | |
3495 | |
3496 case ResourceType_Study: | |
3497 clevel = "STUDY"; | |
3498 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "STUDY"); | |
3499 sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; | |
3500 break; | |
3501 | |
3502 case ResourceType_Series: | |
3503 clevel = "SERIES"; | |
3504 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "SERIES"); | |
3505 sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; | |
3506 break; | |
3507 | |
3508 case ResourceType_Instance: | |
3509 clevel = "IMAGE"; | |
3510 DU_putStringDOElement(dataset, DCM_QueryRetrieveLevel, "IMAGE"); | |
3511 sopClass = UID_FINDStudyRootQueryRetrieveInformationModel; | |
3512 break; | |
3513 | |
3514 default: | |
3515 throw OrthancException(ErrorCode_ParameterOutOfRange); | |
3516 } | |
3517 | |
3518 | |
3519 const char* universal; | |
3520 if (parameters_.GetRemoteManufacturer() == ModalityManufacturer_GE) | |
3521 { | |
3522 universal = "*"; | |
3523 } | |
3524 else | |
3525 { | |
3526 universal = ""; | |
3527 } | |
3528 | |
3529 | |
3530 // Add the expected tags for this query level. | |
3531 // WARNING: Do not reorder or add "break" in this switch-case! | |
3532 switch (level) | |
3533 { | |
3534 case ResourceType_Instance: | |
3535 if (!dataset->tagExists(DCM_SOPInstanceUID)) | |
3536 { | |
3537 DU_putStringDOElement(dataset, DCM_SOPInstanceUID, universal); | |
3538 } | |
3539 | |
3540 case ResourceType_Series: | |
3541 if (!dataset->tagExists(DCM_SeriesInstanceUID)) | |
3542 { | |
3543 DU_putStringDOElement(dataset, DCM_SeriesInstanceUID, universal); | |
3544 } | |
3545 | |
3546 case ResourceType_Study: | |
3547 if (!dataset->tagExists(DCM_AccessionNumber)) | |
3548 { | |
3549 DU_putStringDOElement(dataset, DCM_AccessionNumber, universal); | |
3550 } | |
3551 | |
3552 if (!dataset->tagExists(DCM_StudyInstanceUID)) | |
3553 { | |
3554 DU_putStringDOElement(dataset, DCM_StudyInstanceUID, universal); | |
3555 } | |
3556 | |
3557 case ResourceType_Patient: | |
3558 if (!dataset->tagExists(DCM_PatientID)) | |
3559 { | |
3560 DU_putStringDOElement(dataset, DCM_PatientID, universal); | |
3561 } | |
3562 | |
3563 break; | |
3564 | |
3565 default: | |
3566 throw OrthancException(ErrorCode_ParameterOutOfRange); | |
3567 } | |
3568 | |
3569 assert(clevel != NULL && sopClass != NULL); | |
3570 FindInternal(result, dataset, sopClass, false, clevel); | |
3571 } | |
3572 | |
3573 | |
3574 void Move(const std::string& targetAet, | |
3575 ResourceType level, | |
3576 const DicomMap& findResult) | |
3577 { | |
3578 DicomMap move; | |
3579 switch (level) | |
3580 { | |
3581 case ResourceType_Patient: | |
3582 TestAndCopyTag(move, findResult, DICOM_TAG_PATIENT_ID); | |
3583 break; | |
3584 | |
3585 case ResourceType_Study: | |
3586 TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID); | |
3587 break; | |
3588 | |
3589 case ResourceType_Series: | |
3590 TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID); | |
3591 TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID); | |
3592 break; | |
3593 | |
3594 case ResourceType_Instance: | |
3595 TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID); | |
3596 TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID); | |
3597 TestAndCopyTag(move, findResult, DICOM_TAG_SOP_INSTANCE_UID); | |
3598 break; | |
3599 | |
3600 default: | |
3601 throw OrthancException(ErrorCode_InternalError); | |
3602 } | |
3603 | |
3604 MoveInternal(targetAet, level, move); | |
3605 } | |
3606 | |
3607 | |
3608 void Move(const std::string& targetAet, | |
3609 const DicomMap& findResult) | |
3610 { | |
3611 if (!findResult.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL)) | |
3612 { | |
3613 throw OrthancException(ErrorCode_InternalError); | |
3614 } | |
3615 | |
3616 const std::string tmp = findResult.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).GetContent(); | |
3617 ResourceType level = StringToResourceType(tmp.c_str()); | |
3618 | |
3619 Move(targetAet, level, findResult); | |
3620 } | |
3621 | |
3622 | |
3623 void MovePatient(const std::string& targetAet, | |
3624 const std::string& patientId) | |
3625 { | |
3626 DicomMap query; | |
3627 query.SetValue(DICOM_TAG_PATIENT_ID, patientId, false); | |
3628 MoveInternal(targetAet, ResourceType_Patient, query); | |
3629 } | |
3630 | |
3631 void MoveStudy(const std::string& targetAet, | |
3632 const std::string& studyUid) | |
3633 { | |
3634 DicomMap query; | |
3635 query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false); | |
3636 MoveInternal(targetAet, ResourceType_Study, query); | |
3637 } | |
3638 | |
3639 void MoveSeries(const std::string& targetAet, | |
3640 const std::string& studyUid, | |
3641 const std::string& seriesUid) | |
3642 { | |
3643 DicomMap query; | |
3644 query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false); | |
3645 query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false); | |
3646 MoveInternal(targetAet, ResourceType_Series, query); | |
3647 } | |
3648 | |
3649 void MoveInstance(const std::string& targetAet, | |
3650 const std::string& studyUid, | |
3651 const std::string& seriesUid, | |
3652 const std::string& instanceUid) | |
3653 { | |
3654 DicomMap query; | |
3655 query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid, false); | |
3656 query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid, false); | |
3657 query.SetValue(DICOM_TAG_SOP_INSTANCE_UID, instanceUid, false); | |
3658 MoveInternal(targetAet, ResourceType_Instance, query); | |
3659 } | |
3660 | |
3661 | |
3662 void FindWorklist(DicomFindAnswers& result, | |
3663 ParsedDicomFile& query) | |
3664 { | |
3665 DcmDataset* dataset = query.GetDcmtkObject().getDataset(); | |
3666 const char* sopClass = UID_FINDModalityWorklistInformationModel; | |
3667 | |
3668 FindInternal(result, dataset, sopClass, true, NULL); | |
3669 } | |
3670 }; | |
3671 | |
3672 } | |
3673 | |
3674 | |
3675 TEST(Toto, DISABLED_DicomAssociation) | |
3676 { | |
3677 DicomAssociationParameters params; | |
3678 params.SetLocalApplicationEntityTitle("ORTHANC"); | |
3679 params.SetRemoteApplicationEntityTitle("PACS"); | |
3680 params.SetRemotePort(2001); | |
3681 | |
3682 #if 0 | |
3683 DicomAssociation assoc; | |
3684 assoc.ProposeGenericPresentationContext(UID_StorageCommitmentPushModelSOPClass); | |
3685 assoc.ProposeGenericPresentationContext(UID_VerificationSOPClass); | |
3686 assoc.ProposePresentationContext(UID_ComputedRadiographyImageStorage, | |
3687 DicomTransferSyntax_JPEGProcess1); | |
3688 assoc.ProposePresentationContext(UID_ComputedRadiographyImageStorage, | |
3689 DicomTransferSyntax_JPEGProcess2_4); | |
3690 assoc.ProposePresentationContext(UID_ComputedRadiographyImageStorage, | |
3691 DicomTransferSyntax_JPEG2000); | |
3692 | |
3693 assoc.Open(params); | |
3694 | |
3695 int presID = ASC_findAcceptedPresentationContextID(&assoc.GetDcmtkAssociation(), UID_ComputedRadiographyImageStorage); | |
3696 printf(">> %d\n", presID); | |
3697 | |
3698 std::map<DicomTransferSyntax, uint8_t> pc; | |
3699 printf(">> %d\n", assoc.LookupAcceptedPresentationContext(pc, UID_ComputedRadiographyImageStorage)); | |
3700 | |
3701 for (std::map<DicomTransferSyntax, uint8_t>::const_iterator | |
3702 it = pc.begin(); it != pc.end(); ++it) | |
3703 { | |
3704 printf("[%s] => %d\n", GetTransferSyntaxUid(it->first), it->second); | |
3705 } | |
3706 #else | |
3707 DicomControlUserConnection assoc(params); | |
3708 | |
3709 try | |
3710 { | |
3711 printf(">> %d\n", assoc.Echo()); | |
3712 } | |
3713 catch (OrthancException&) | |
3714 { | |
3715 } | |
3716 | |
3717 params.SetRemoteApplicationEntityTitle("PACS"); | |
3718 params.SetRemotePort(2000); | |
3719 assoc.SetParameters(params); | |
3720 printf(">> %d\n", assoc.Echo()); | |
3721 | |
3722 #endif | |
3723 } | |
3724 | |
3725 | |
3726 #endif |