changeset 1859:58681a5c727b

overlay: display ContentDate/ContentTime instead of StudyDate if available + new 'TimeFormat' option
author Alain Mazy <am@osimis.io>
date Mon, 20 Sep 2021 17:08:23 +0200
parents be88206f8d78
children b8f54ab47b3b
files Applications/StoneWebViewer/BuildInstructions.txt Applications/StoneWebViewer/NEWS Applications/StoneWebViewer/WebApplication/app.js Applications/StoneWebViewer/WebApplication/configuration.json Applications/StoneWebViewer/WebApplication/index.html Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp
diffstat 6 files changed, 154 insertions(+), 18 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Applications/StoneWebViewer/BuildInstructions.txt	Mon Sep 20 17:08:23 2021 +0200
@@ -0,0 +1,68 @@
+Build & develop instructions
+============================
+
+Prerequisites:
+-------------
+
+- Install the usual build tools to build an Orthanc plugin (a good reference is the orthanc base image Dockerfile): https://github.com/jodogne/OrthancDocker/blob/master/base/Dockerfile
+- Install EMSDK (a good reference is the wasm-builder image Dockerfile): https://github.com/jodogne/OrthancDocker/tree/master/wasm-builder
+
+Create your build environment:
+-----------------------------
+
+mkdir ~/dev
+cd ~/dev
+hg clone https://hg.orthanc-server.com/orthanc/
+hg clone https://hg.orthanc-server.com/orthanc-stone/
+hg clone https://hg.orthanc-server.com/orthanc-dicomweb/
+mkdir -p ~/dev/build/orthanc
+mkdir -p ~/dev/build/orthanc-dicomweb
+mkdir -p ~/dev/build/wasm-stone-viewer
+mkdir -p ~/dev/build/stone-viewer-plugin
+
+# build orthanc
+cd ~/dev/build/orthanc
+cmake -DCMAKE_BUILD_TYPE=Release -DUSE_SYSTEM_CIVETWEB=OFF -DALLOW_DOWNLOADS=ON ../../orthanc/OrthancServer
+make -j 6
+
+# build orthanc-dicomweb
+cd ~/dev/build/orthanc-dicomweb
+cmake -DCMAKE_BUILD_TYPE=Release -DSTATIC_BUILD -DALLOW_DOWNLOADS=ON ../../orthanc-dicomweb
+make -j 6
+
+# build the StoneViewer WASM code
+cd ~/dev/build/wasm-stone-viewer
+cmake ../../orthanc-stone/Applications/StoneWebViewer/WebAssembly -DLIBCLANG=/usr/lib/x86_64-linux-gnu/libclang-10.so -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=${EMSDK}/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake -DALLOW_DOWNLOADS=ON -G Ninja
+ninja install
+
+# build the StoneViewer Plugin
+cd ~/dev/build/stone-viewer-plugin
+cmake ../../orthanc-stone/Applications/StoneWebViewer/Plugin -DUSE -DCMAKE_BUILD_TYPE=Release -DSTATIC_BUILD=ON -DALLOW_DOWNLOADS=ON
+make -j 6
+
+
+Updating the code:
+-----------------
+
+To avoid rebuilding the plugin everytime you update the HTML/CSS/JS or C++ code from the WebAssembly, you can use the ServeFolders plugin
+to host this part of the code.  Here's a sample configuration file that works well for StoneViewer development:
+
+{
+    "AuthenticationEnabled": false,
+    "HttpPort": 8043,
+    "Plugins": [
+        "/home/alain/dev/build/stone-viewer-plugin/libStoneWebViewer.so",
+        "/home/alain/dev/build/orthanc-dicomweb/libOrthancDicomWeb.so",
+        "/home/alain/dev/build/orthanc/libServeFolders.so"
+    ],
+
+    "ServeFolders": {
+        "/stone-webviewer-live" : "/home/alain/o/orthanc-stone/wasm-binaries/StoneWebViewer/"
+    }
+}
+
+Everytime you modify the HTML/CSS/JS or C++ code from the WebAssembly, you don't need to restart Orthanc and can simply run
+cd ~/dev/build/wasm-stone-viewer && ninja install
+Then, the viewer is available on http://localhost:8043/stone-webviewer-live/index.html?study=1.2.3.4.5
+
+If you modify the plugin code, you must of course rebuild the plugin and restart Orthanc
\ No newline at end of file
--- a/Applications/StoneWebViewer/NEWS	Mon Sep 06 22:17:32 2021 +0200
+++ b/Applications/StoneWebViewer/NEWS	Mon Sep 20 17:08:23 2021 +0200
@@ -1,6 +1,12 @@
 Pending changes in the mainline
 ===============================
 
+* In the top right overlay, display ContentDate/ContentTime if they are 
+  available in the instance.  If not, StudyDate is displayed (previous
+  behavior)
+* New configuration options:
+  - "TimeFormat" to control the way Dicom Times are displayed.
+
 
 Version 2.2 (2021-08-31)
 ========================
--- a/Applications/StoneWebViewer/WebApplication/app.js	Mon Sep 06 22:17:32 2021 +0200
+++ b/Applications/StoneWebViewer/WebApplication/app.js	Mon Sep 20 17:08:23 2021 +0200
@@ -150,7 +150,9 @@
       videoUri: '',
       windowingCenter: 0, 
       windowingWidth: 0,
-      instanceNumber: 0
+      instanceNumber: 0,
+      contentDate: '',
+      contentTime: '',
     }
   },
   watch: {
@@ -171,7 +173,9 @@
       this.windowingCenter = 0;
       this.windowingWidth = 0;
       this.instanceNumber = 0;
-      
+      this.contentDate = '';
+      this.contentTime = ''
+
       if (this.cineTimeoutId !== null) {
         clearTimeout(this.cineTimeoutId);
         this.cineTimeoutId = null;
@@ -255,6 +259,8 @@
         that.numberOfFrames = args.detail.numberOfFrames;
         that.quality = args.detail.quality;
         that.instanceNumber = args.detail.instanceNumber;
+        that.contentDate = args.detail.contentDate;
+        that.contentTime = args.detail.contentTime;
       }
     });
 
@@ -938,6 +944,43 @@
       }
     },
 
+    FormatTime: function(time)
+    {
+      if (time === undefined ||
+        time.length == 0) {
+        return '';
+      }
+      else {
+        var format = this.globalConfiguration['TimeFormat'];
+        if (format === undefined) {
+          // No configuration for the date format, use the DICOM tag as such
+          return time;
+        }
+        else {
+          var timeRegexHMS = /([0-9]{2})([0-9]{2})([0-9]{2})/;
+          var timeRegexHMSms = /([0-9]{2})([0-9]{2})([0-9]{2}).([0-9]*)/
+          var m = time.match(timeRegexHMSms);
+          if (m) {
+            format = format.replace(/hh/g, m[1]).replace(/mm/g, m[2]).replace(/ss/g, m[3]);
+            if (format.indexOf('f') != -1) { // format expects ms
+              return format.replace(/f/g, m[4])
+            } else {
+              return format;
+            }
+          }
+          var m = time.match(timeRegexHMS);
+          if (m) {
+            format = format.replace(/hh/g, m[1]).replace(/mm/g, m[2]).replace(/ss/g, m[3]);
+            if (format.indexOf('f') != -1) { // format expects ms but we could not capture one
+              return format.replace(/.f/g, '')
+            }
+          }
+
+          return time;
+        }
+      }
+    },
+
     DownloadJpeg: function()
     {
       var canvas = document.getElementById(this.GetActiveCanvas());
--- a/Applications/StoneWebViewer/WebApplication/configuration.json	Mon Sep 06 22:17:32 2021 +0200
+++ b/Applications/StoneWebViewer/WebApplication/configuration.json	Mon Sep 20 17:08:23 2021 +0200
@@ -9,6 +9,15 @@
     // "DateFormat" : "DD/MM/YYYY",
 
     /**
+     * Defines how times are displayed in the UI. If this option is not
+     * set, the DICOM tags will be displayed as such. "hh" will be
+     * replaced by the hour, "mm" by the minutes, "ss" by the seconds
+     * and ".f" by the fractions of seconds.
+     **/
+    // "TimeFormat" : "hh:mm:ss.f",
+
+
+    /**
      * This option allows you to define windowing presets.
      * For each preset, you must provide a name, the window width
      * and window center.
--- a/Applications/StoneWebViewer/WebApplication/index.html	Mon Sep 06 22:17:32 2021 +0200
+++ b/Applications/StoneWebViewer/WebApplication/index.html	Mon Sep 20 17:08:23 2021 +0200
@@ -672,7 +672,8 @@
                   </div>
                   <div v-if="'tags' in content.series" class="wv-overlay-topright">
                     {{ content.series.tags[STUDY_DESCRIPTION] }}<br/>
-                    {{ app.FormatDate(content.series.tags[STUDY_DATE]) }}<br/>
+                    <span v-if="contentDate !== undefined && contentDate != ''">{{ app.FormatDate(contentDate) }} <span v-show="contentTime != ''">{{ app.FormatTime(contentTime) }}</span><br/></span>
+                    <span v-else="contentDate === undefined || contentDate == ''">{{ app.FormatDate(content.series.tags[STUDY_DATE]) }}<br/></span>
                     {{ content.series.tags[SERIES_NUMBER] }} | {{ content.series.tags[SERIES_DESCRIPTION] }}
                   </div>
                   <div class="wv-overlay-bottomleft wvPrintExclude" style="bottom: 0px">
@@ -813,9 +814,6 @@
         </div>
       </div>
     </script>
-
-    
-
     <script src="js/jquery-3.4.1.min.js"></script>
     <script src="js/bootstrap.min.js"></script>
     <script src="js/vue.min.js"></script>
--- a/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp	Mon Sep 06 22:17:32 2021 +0200
+++ b/Applications/StoneWebViewer/WebAssembly/StoneWebViewer.cpp	Mon Sep 20 17:08:23 2021 +0200
@@ -1450,7 +1450,9 @@
                                     size_t currentFrame,
                                     size_t countFrames,
                                     DisplayedFrameQuality quality,
-                                    unsigned int instanceNumber) = 0;
+                                    unsigned int instanceNumber,
+                                    const std::string& contentDate,
+                                    const std::string& contentTime) = 0;
 
     // "click" is a 3D vector in world coordinates
     virtual void SignalCrosshair(const ViewerViewport& viewport,
@@ -1972,13 +1974,15 @@
     {
       const Orthanc::DicomMap& instance = frames_->GetInstanceOfFrame(cursor_->GetCurrentIndex()).GetTags();
 
-      uint32_t instanceNumber;
-      if (!instance.ParseUnsignedInteger32(instanceNumber, Orthanc::DICOM_TAG_INSTANCE_NUMBER))
-      {
-        instanceNumber = 0;
-      }
-      
-      observer_->SignalFrameUpdated(*this, cursorIndex, frames_->GetFramesCount(), quality, instanceNumber);
+      uint32_t instanceNumber = 0;
+      std::string contentDate;
+      std::string contentTime;
+
+      instance.ParseUnsignedInteger32(instanceNumber, Orthanc::DICOM_TAG_INSTANCE_NUMBER);
+      instance.LookupStringValue(contentDate, Orthanc::DicomTag(0x0008, 0x0023), false);
+      instance.LookupStringValue(contentTime, Orthanc::DicomTag(0x0008, 0x0033), false);
+
+      observer_->SignalFrameUpdated(*this, cursorIndex, frames_->GetFramesCount(), quality, instanceNumber, contentDate, contentTime);
     }
   }
   
@@ -2575,7 +2579,7 @@
     if (observer_.get() != NULL)
     {
       observer_->SignalFrameUpdated(*this, cursor_->GetCurrentIndex(),
-                                    frames_->GetFramesCount(), DisplayedFrameQuality_None, 0);
+                                    frames_->GetFramesCount(), DisplayedFrameQuality_None, 0, "", "");
     }
 
     centralPhysicalWidth_ = 1;
@@ -3257,7 +3261,9 @@
                                   size_t currentFrame,
                                   size_t countFrames,
                                   DisplayedFrameQuality quality,
-                                  unsigned int instanceNumber) ORTHANC_OVERRIDE
+                                  unsigned int instanceNumber,
+                                  const std::string& contentDate,
+                                  const std::string& contentTime) ORTHANC_OVERRIDE
   {
     EM_ASM({
         const customEvent = document.createEvent("CustomEvent");
@@ -3266,13 +3272,19 @@
                                         "currentFrame" : $1,
                                         "numberOfFrames" : $2,
                                         "quality" : $3,
-                                        "instanceNumber" : $4 });
+                                        "instanceNumber" : $4,
+                                        "contentDate" : UTF8ToString($5),
+                                        "contentTime" : UTF8ToString($6),
+                                         });
         window.dispatchEvent(customEvent);
       },
       viewport.GetCanvasId().c_str(),
       static_cast<int>(currentFrame),
       static_cast<int>(countFrames),
-      quality, instanceNumber);
+      quality, 
+      instanceNumber, 
+      contentDate.c_str(),
+      contentTime.c_str());
 
     UpdateReferenceLines();
   }