changeset 91:c98502a806f2

passing the project ID to the viewer URL generator
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 30 Jul 2026 18:03:07 +0200
parents 3a6a279a8ff9
children cb207fadb7dd
files Sources/EducationRestApi.cpp Sources/EducationToolbox.cpp Sources/EducationToolbox.h Sources/LTI/LTIRoutes.cpp Sources/OrthancDatabase.cpp WebApplication/dashboard.html WebApplication/dashboard.js WebApplication/deep.js WebApplication/list-projects.html WebApplication/list-projects.js WebApplication/toolbox.js
diffstat 11 files changed, 110 insertions(+), 59 deletions(-) [+]
line wrap: on
line diff
--- a/Sources/EducationRestApi.cpp	Thu Jul 30 16:10:10 2026 +0200
+++ b/Sources/EducationRestApi.cpp	Thu Jul 30 18:03:07 2026 +0200
@@ -176,6 +176,7 @@
 {
   assert(user.GetRole() == Role_Guest);
 
+  const std::string projectId = Orthanc::SerializationToolbox::ReadString(body, "project");
   const ViewerType viewer = ParseViewerType(Orthanc::SerializationToolbox::ReadString(body, "viewer"));
 
   Json::Value resource;
@@ -184,7 +185,7 @@
     throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
   }
 
-  const std::string viewerUrl = EducationToolbox::GenerateViewerUrl(viewer, resource);
+  const std::string viewerUrl = EducationToolbox::GenerateViewerUrlGeneric(projectId, viewer, resource);
 
   Json::Value answer;
 
--- a/Sources/EducationToolbox.cpp	Thu Jul 30 16:10:10 2026 +0200
+++ b/Sources/EducationToolbox.cpp	Thu Jul 30 18:03:07 2026 +0200
@@ -24,19 +24,28 @@
 
 #include "EducationToolbox.h"
 
+#include "HttpToolbox.h"
+
 #include <OrthancException.h>
+#include <SerializationToolbox.h>
+#include <Toolbox.h>
+
+#include <cassert>
 
 
-static std::string FormatWSITitle(const std::string& title)
+static std::string FormatGetArgument(const std::string& key,
+                                     const std::string& value)
 {
-  if (title.empty())
+  assert(!key.empty());
+
+  if (value.empty())
   {
     return "";
   }
   else
   {
     /**
-     * The following implementation is correct if title is an UTF-8
+     * The following implementation is correct if value is an UTF-8
      * string: "When a new URI scheme defines a component that
      * represents textual data consisting of characters from the
      * Universal Character Set [UCS], the data should first be encoded
@@ -46,13 +55,14 @@
      * https://www.rfc-editor.org/rfc/rfc3986
      **/
     std::string tmp;
-    Orthanc::Toolbox::UriEncode(tmp, title);
-    return "&description=" + tmp;
+    Orthanc::Toolbox::UriEncode(tmp, value);
+    return "&" + key + "=" + tmp;
   }
 }
 
 
-static std::string GenerateStudyViewerUrl(ViewerType viewer,
+static std::string GenerateStudyViewerUrl(const std::string& projectId,
+                                          ViewerType viewer,
                                           const std::string& studyId,
                                           const std::string& studyInstanceUid,
                                           const std::string& title)
@@ -83,7 +93,8 @@
 }
 
 
-static std::string GenerateSeriesViewerUrl(ViewerType viewer,
+static std::string GenerateSeriesViewerUrl(const std::string& projectId,
+                                           ViewerType viewer,
                                            const std::string& seriesId,
                                            const std::string& studyInstanceUid,
                                            const std::string& seriesInstanceUid,
@@ -95,7 +106,7 @@
       return "stone-webviewer/index.html?study=" + studyInstanceUid + "&series=" + seriesInstanceUid;
 
     case ViewerType_WholeSlideImaging:
-      return "wsi/app/viewer.html?series=" + seriesId + FormatWSITitle(title);
+      return "wsi/app/viewer.html?series=" + seriesId + FormatGetArgument("title", title);
 
     case ViewerType_VolView:
       return "volview/index.html?names=[archive.zip]&urls=[../series/" + seriesId + "/archive]";
@@ -106,7 +117,8 @@
 }
 
 
-static std::string GenerateInstanceViewerUrl(ViewerType viewer,
+static std::string GenerateInstanceViewerUrl(const std::string& projectId,
+                                             ViewerType viewer,
                                              const std::string& instanceId,
                                              const std::string& studyInstanceUid,
                                              const std::string& seriesInstanceUid,
@@ -116,7 +128,7 @@
   switch (viewer)
   {
     case ViewerType_WholeSlideImaging:
-      return "wsi/app/viewer.html?instance=" + instanceId + FormatWSITitle(title);
+      return "wsi/app/viewer.html?instance=" + instanceId + FormatGetArgument("title", title);
 
     default:
       throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
@@ -124,36 +136,51 @@
 }
 
 
+static const char* const KEY_LEVEL = "level";
+static const char* const KEY_PROJECT = "project";
+static const char* const KEY_RESOURCE_ID = "resource-id";
+static const char* const KEY_SERIES_INSTANCE_UID = "series-instance-uid";
+static const char* const KEY_SOP_INSTANCE_UID = "sop-instance-uid";
+static const char* const KEY_STUDY_INSTANCE_UID = "study-instance-uid";
+static const char* const KEY_TITLE = "title";
+
+
 namespace EducationToolbox
 {
-  std::string GenerateViewerUrl(ViewerType viewer,
-                                const std::map<std::string, std::string>& resource)
+  std::string GenerateViewerUrlForDeepLinking(ViewerType viewer,
+                                              const std::map<std::string, std::string>& resource)
   {
-    const std::string levelString = HttpToolbox::ReadMandatoryString(resource, "level");
+    const std::string projectId = HttpToolbox::ReadMandatoryString(resource, KEY_PROJECT);
+    if (projectId.empty())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+
+    const std::string levelString = HttpToolbox::ReadMandatoryString(resource, KEY_LEVEL);
     const Orthanc::ResourceType level = Orthanc::StringToResourceType(levelString.c_str());
 
     switch (level)
     {
       case Orthanc::ResourceType_Study:
-        return GenerateStudyViewerUrl(viewer,
-                                      HttpToolbox::ReadMandatoryString(resource, "resource-id"),
-                                      HttpToolbox::ReadMandatoryString(resource, "study-instance-uid"),
-                                      HttpToolbox::ReadMandatoryString(resource, "title"));
+        return GenerateStudyViewerUrl(projectId, viewer,
+                                      HttpToolbox::ReadMandatoryString(resource, KEY_RESOURCE_ID),
+                                      HttpToolbox::ReadMandatoryString(resource, KEY_STUDY_INSTANCE_UID),
+                                      HttpToolbox::ReadMandatoryString(resource, KEY_TITLE));
 
       case Orthanc::ResourceType_Series:
-        return GenerateSeriesViewerUrl(viewer,
-                                       HttpToolbox::ReadMandatoryString(resource, "resource-id"),
-                                       HttpToolbox::ReadMandatoryString(resource, "study-instance-uid"),
-                                       HttpToolbox::ReadMandatoryString(resource, "series-instance-uid"),
-                                       HttpToolbox::ReadMandatoryString(resource, "title"));
+        return GenerateSeriesViewerUrl(projectId, viewer,
+                                       HttpToolbox::ReadMandatoryString(resource, KEY_RESOURCE_ID),
+                                       HttpToolbox::ReadMandatoryString(resource, KEY_STUDY_INSTANCE_UID),
+                                       HttpToolbox::ReadMandatoryString(resource, KEY_SERIES_INSTANCE_UID),
+                                       HttpToolbox::ReadMandatoryString(resource, KEY_TITLE));
 
       case Orthanc::ResourceType_Instance:
-        return GenerateInstanceViewerUrl(viewer,
-                                         HttpToolbox::ReadMandatoryString(resource, "resource-id"),
-                                         HttpToolbox::ReadMandatoryString(resource, "study-instance-uid"),
-                                         HttpToolbox::ReadMandatoryString(resource, "series-instance-uid"),
-                                         HttpToolbox::ReadMandatoryString(resource, "sop-instance-uid"),
-                                         HttpToolbox::ReadMandatoryString(resource, "title"));
+        return GenerateInstanceViewerUrl(projectId, viewer,
+                                         HttpToolbox::ReadMandatoryString(resource, KEY_RESOURCE_ID),
+                                         HttpToolbox::ReadMandatoryString(resource, KEY_STUDY_INSTANCE_UID),
+                                         HttpToolbox::ReadMandatoryString(resource, KEY_SERIES_INSTANCE_UID),
+                                         HttpToolbox::ReadMandatoryString(resource, KEY_SOP_INSTANCE_UID),
+                                         HttpToolbox::ReadMandatoryString(resource, KEY_TITLE));
 
       default:
         throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
@@ -161,26 +188,38 @@
   }
 
 
-  std::string GenerateViewerUrl(ViewerType viewer,
-                                const Json::Value& resource)
+  std::string GenerateViewerUrlGeneric(const std::string& projectId,
+                                       ViewerType viewer,
+                                       const Json::Value& resource)
   {
-    std::map<std::string, std::string> args;
+    const std::string levelString = Orthanc::SerializationToolbox::ReadString(resource, KEY_LEVEL);
+    const Orthanc::ResourceType level = Orthanc::StringToResourceType(levelString.c_str());
 
-    if (resource.type() != Json::objectValue)
+    switch (level)
     {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
-    }
+      case Orthanc::ResourceType_Study:
+        return GenerateStudyViewerUrl(projectId, viewer,
+                                      Orthanc::SerializationToolbox::ReadString(resource, KEY_RESOURCE_ID),
+                                      Orthanc::SerializationToolbox::ReadString(resource, KEY_STUDY_INSTANCE_UID),
+                                      Orthanc::SerializationToolbox::ReadString(resource, KEY_TITLE));
 
-    Json::Value::Members members = resource.getMemberNames();
-    for (size_t i = 0; i < members.size(); i++)
-    {
-      const Json::Value& value = resource[members[i]];
-      if (value.type() == Json::stringValue)
-      {
-        args[members[i]] = value.asString();
-      }
+      case Orthanc::ResourceType_Series:
+        return GenerateSeriesViewerUrl(projectId, viewer,
+                                       Orthanc::SerializationToolbox::ReadString(resource, KEY_RESOURCE_ID),
+                                       Orthanc::SerializationToolbox::ReadString(resource, KEY_STUDY_INSTANCE_UID),
+                                       Orthanc::SerializationToolbox::ReadString(resource, KEY_SERIES_INSTANCE_UID),
+                                       Orthanc::SerializationToolbox::ReadString(resource, KEY_TITLE));
+
+      case Orthanc::ResourceType_Instance:
+        return GenerateInstanceViewerUrl(projectId, viewer,
+                                         Orthanc::SerializationToolbox::ReadString(resource, KEY_RESOURCE_ID),
+                                         Orthanc::SerializationToolbox::ReadString(resource, KEY_STUDY_INSTANCE_UID),
+                                         Orthanc::SerializationToolbox::ReadString(resource, KEY_SERIES_INSTANCE_UID),
+                                         Orthanc::SerializationToolbox::ReadString(resource, KEY_SOP_INSTANCE_UID),
+                                         Orthanc::SerializationToolbox::ReadString(resource, KEY_TITLE));
+
+      default:
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
     }
-
-    return GenerateViewerUrl(viewer, args);
   }
 }
--- a/Sources/EducationToolbox.h	Thu Jul 30 16:10:10 2026 +0200
+++ b/Sources/EducationToolbox.h	Thu Jul 30 18:03:07 2026 +0200
@@ -24,14 +24,19 @@
 
 #pragma once
 
-#include "Permissions/AuthenticatedUser.h"
+#include "EducationEnumerations.h"
+
+#include <json/value.h>
+#include <map>
+#include <string>
 
 
 namespace EducationToolbox
 {
-  std::string GenerateViewerUrl(ViewerType viewer,
-                                const std::map<std::string, std::string>& resource);
+  std::string GenerateViewerUrlForDeepLinking(ViewerType viewer,
+                                              const std::map<std::string, std::string>& resource);
 
-  std::string GenerateViewerUrl(ViewerType viewer,
-                                const Json::Value& resource);
+  std::string GenerateViewerUrlGeneric(const std::string& projectId,
+                                       ViewerType viewer,
+                                       const Json::Value& resource);
 }
--- a/Sources/LTI/LTIRoutes.cpp	Thu Jul 30 16:10:10 2026 +0200
+++ b/Sources/LTI/LTIRoutes.cpp	Thu Jul 30 18:03:07 2026 +0200
@@ -354,7 +354,7 @@
       const ViewerType viewer = ParseViewerType(HttpToolbox::ReadMandatoryString(args, "viewer"));
 
       link["title"] = HttpToolbox::ReadMandatoryString(args, "title");
-      linkUrl = Orthanc::Toolbox::JoinUri("../..", EducationToolbox::GenerateViewerUrl(viewer, args));
+      linkUrl = Orthanc::Toolbox::JoinUri("../..", EducationToolbox::GenerateViewerUrlForDeepLinking(viewer, args));
     }
     else if (type == "project")
     {
@@ -433,7 +433,7 @@
     }
 
     const ViewerType viewer = ParseViewerType(HttpToolbox::ReadMandatoryString(args, "viewer"));
-    const std::string viewerUrl = Orthanc::Toolbox::JoinUri("../..", EducationToolbox::GenerateViewerUrl(viewer, args));
+    const std::string viewerUrl = Orthanc::Toolbox::JoinUri("../..", EducationToolbox::GenerateViewerUrlForDeepLinking(viewer, args));
 
     // We manually reimplement "OrthancPluginRedirect()", otherwise "Set-Cookie" has no effect
     OrthancPluginSetHttpHeader(OrthancPlugins::GetGlobalContext(), output, "Location", viewerUrl.c_str());
--- a/Sources/OrthancDatabase.cpp	Thu Jul 30 16:10:10 2026 +0200
+++ b/Sources/OrthancDatabase.cpp	Thu Jul 30 18:03:07 2026 +0200
@@ -521,6 +521,7 @@
     target["description"] = project.GetDescription();
     target["policy"] = EnumerationToString(project.GetPolicy());
     target["primary_viewer"] = EnumerationToString(project.GetPrimaryViewer());
+    target["project"] = projectId;
 
     std::set<ViewerType> viewers;
     project.GetAllViewers(viewers);
--- a/WebApplication/dashboard.html	Thu Jul 30 16:10:10 2026 +0200
+++ b/WebApplication/dashboard.html	Thu Jul 30 18:03:07 2026 +0200
@@ -249,7 +249,7 @@
                                   <i class="fa fa-folder-open-o"></i> Open Orthanc Explorer
                                 </button>
                                 <button type="button" class="btn btn-success btn-sm"
-                                        @click="openViewer(selectedViewer, resource)">
+                                        @click="openViewer('' /* no known project at this time */, selectedViewer, resource)">
                                   <i class="fa fa-eye me-1"></i> Open DICOM viewer
                                 </button>
                               </p>
@@ -397,7 +397,7 @@
                               </button>
                               <button type="button" class="btn btn-success"
                                       data-bs-toggle="tooltip" data-bs-placement="top" title="Open the DICOM viewer"
-                                      @click="openViewer(selectedViewer, resource)">
+                                      @click="openViewer(projectIdForContent, selectedViewer, resource)">
                                 <i class="fa fa-eye"></i>
                               </button>
                             </td>
--- a/WebApplication/dashboard.js	Thu Jul 30 16:10:10 2026 +0200
+++ b/WebApplication/dashboard.js	Thu Jul 30 18:03:07 2026 +0200
@@ -414,7 +414,7 @@
 
     copyViewerToClipboard: function(resource) {
       var that = this;
-      doCopyViewerToClipboard(this.selectedViewer, resource, function() {
+      doCopyViewerToClipboard(this.projectIdForContent, this.selectedViewer, resource, function() {
         that.clearClipboardIcons();
         that.checkClipboardIcon(that.getClipboardIconId(resource));
       });
--- a/WebApplication/deep.js	Thu Jul 30 16:10:10 2026 +0200
+++ b/WebApplication/deep.js	Thu Jul 30 18:03:07 2026 +0200
@@ -28,6 +28,7 @@
     return {
       project_name: '',
       project_description: '',
+      project_id: '',
       filter: '',
       resources: [],
       info: {},
@@ -50,6 +51,7 @@
       params.append('study-instance-uid', resource['study-instance-uid']);
       params.append('viewer', this.selected_viewer);
       params.append('title', resource.title);
+      params.append('project', this.project_id);
 
       if (full) {
         // Add the additional parameters needed to create the deep link
@@ -82,6 +84,7 @@
           that.resources = sortObjectsByField(res.data.resources, 'title');
           that.selected_viewer = res.data.primary_viewer;
           that.viewers = res.data.secondary_viewers;
+          that.project_id = res.data.project;
 
           that.resources.forEach((resource) => {
             axios.get(resource.preview_url, {
--- a/WebApplication/list-projects.html	Thu Jul 30 16:10:10 2026 +0200
+++ b/WebApplication/list-projects.html	Thu Jul 30 18:03:07 2026 +0200
@@ -128,7 +128,7 @@
                 </button>
                 <button type="button" class="btn btn-success"
                         data-bs-toggle="tooltip" data-bs-placement="top" title="Open the DICOM viewer"
-                        @click="openViewer(selectedViewer, resource)">
+                        @click="openViewer(selectedProjectId, selectedViewer, resource)">
                   <i class="fa fa-eye"></i>
                 </button>
               </td>
--- a/WebApplication/list-projects.js	Thu Jul 30 16:10:10 2026 +0200
+++ b/WebApplication/list-projects.js	Thu Jul 30 18:03:07 2026 +0200
@@ -143,7 +143,7 @@
 
     copyViewerToClipboard: function(resource) {
       var that = this;
-      doCopyViewerToClipboard(this.selectedViewer, resource, function() {
+      doCopyViewerToClipboard(this.selectedProjectId, this.selectedViewer, resource, function() {
         // Clear any "check" icon
         const icons = document.getElementsByClassName('clipboard-icon');
         for (var i = 0; i < icons.length; i++) {
--- a/WebApplication/toolbox.js	Thu Jul 30 16:10:10 2026 +0200
+++ b/WebApplication/toolbox.js	Thu Jul 30 18:03:07 2026 +0200
@@ -47,8 +47,9 @@
 }
 
 
-function openViewer(viewer, resource) {
+function openViewer(projectId, viewer, resource) {
   axios.post('../api/resource-viewer-url', {
+    project: projectId,
     viewer: viewer,
     resource: resource
   })
@@ -73,8 +74,9 @@
 }
 
 
-function doCopyViewerToClipboard(viewer, resource, callback) {
+function doCopyViewerToClipboard(projectId, viewer, resource, callback) {
   axios.post('../api/resource-viewer-url', {
+    project: projectId,
     viewer: viewer,
     resource: resource
   })