fw4spl
ioDicomWeb/src/ioDicomWeb/SSliceIndexDicomPullerEditor.cpp
1 /* ***** BEGIN LICENSE BLOCK *****
2  * FW4SPL - Copyright (C) IRCAD, 2018.
3  * Distributed under the terms of the GNU Lesser General Public License (LGPL) as
4  * published by the Free Software Foundation.
5  * ****** END LICENSE BLOCK ****** */
6 
7 #include "ioDicomWeb/SSliceIndexDicomPullerEditor.hpp"
8 
9 #include <fwCom/Signal.hpp>
10 #include <fwCom/Signal.hxx>
11 #include <fwCom/Signals.hpp>
12 #include <fwCom/Slot.hpp>
13 #include <fwCom/Slots.hpp>
14 #include <fwCom/Slots.hxx>
15 
16 #include <fwData/Array.hpp>
17 #include <fwData/Composite.hpp>
18 #include <fwData/Image.hpp>
19 #include <fwData/Integer.hpp>
20 
21 #include <fwDataTools/fieldHelper/Image.hpp>
22 #include <fwDataTools/helper/Array.hpp>
23 #include <fwDataTools/helper/Composite.hpp>
24 
25 #include <fwGui/dialog/MessageDialog.hpp>
26 
27 #include <fwGuiQt/container/QtContainer.hpp>
28 
29 #include <fwMedData/DicomSeries.hpp>
30 #include <fwMedData/ImageSeries.hpp>
31 #include <fwMedData/SeriesDB.hpp>
32 
33 #include <fwMedDataTools/helper/SeriesDB.hpp>
34 
35 #include <fwNetworkIO/exceptions/Base.hpp>
36 
37 #include <fwPreferences/helper.hpp>
38 
39 #include <fwServices/macros.hpp>
40 #include <fwServices/registry/ActiveWorkers.hpp>
41 #include <fwServices/registry/ObjectService.hpp>
42 
43 #include <fwThread/Timer.hpp>
44 
45 #include <fwTools/System.hpp>
46 
47 #include <boost/asio/placeholders.hpp>
48 #include <boost/filesystem/fstream.hpp>
49 #include <boost/filesystem/operations.hpp>
50 #include <boost/foreach.hpp>
51 
52 #include <QApplication>
53 #include <QComboBox>
54 #include <QHBoxLayout>
55 #include <QMouseEvent>
56 
57 #include <iterator>
58 
59 namespace ioDicomWeb
60 {
61 
62 //------------------------------------------------------------------------------
63 
65  m_delay(500)
66 {
67 }
68 
69 //------------------------------------------------------------------------------
70 
72 {
73 }
74 
75 //------------------------------------------------------------------------------
76 
78 {
80 
81  ::fwRuntime::ConfigurationElement::sptr config = m_configuration->findConfigurationElement("config");
82  SLM_ASSERT("The service ::ioDicomWeb::SPacsConfigurationInitializer must have "
83  "a \"config\" element.", config);
84 
85  bool success;
86 
87  // Reader
88  ::boost::tie(success, m_dicomReaderType) = config->getSafeAttributeValue("dicomReader");
89  SLM_ASSERT("It should be a \"dicomReader\" tag in the ::ioDicomWeb::SSliceIndexDicomPullerEditor "
90  "config element.", success);
91 
92  // Reader configuration
93  ::fwRuntime::ConfigurationElement::sptr readerConfig = config->findConfigurationElement("dicomReaderConfig");
94  m_readerConfig =
95  (readerConfig && readerConfig->size() == 1) ? readerConfig->getElements()[0] : nullptr;
96 
97  // Delay
98  std::string delayStr;
99  ::boost::tie(success, delayStr) = config->getSafeAttributeValue("delay");
100  if(success)
101  {
102  m_delay = ::boost::lexical_cast< unsigned int >(delayStr);
103  }
104 
105  if(m_delayTimer && m_delayTimer->isRunning())
106  {
107  m_delayTimer->stop();
108  m_delayTimer.reset();
109  }
110 
111  m_delayTimer = m_associatedWorker->createTimer();
112  m_delayTimer->setFunction( [ = ]()
113  {
114  this->triggerNewSlice();
115  } );
116 
117  m_delayTimer->setOneShot(true);
118 }
119 
120 // -----------------------------------------------------------------------------
121 
122 std::string SSliceIndexDicomPullerEditor::getPreferenceKey(const std::string& key) const
123 {
124  std::string keyResult;
125  const size_t first = key.find('%');
126  const size_t last = key.rfind('%');
127  if (first == 0 && last == key.size() - 1)
128  {
129  keyResult = key.substr(1, key.size() - 2);
130  }
131  return keyResult;
132 }
133 
134 //------------------------------------------------------------------------------
135 
137 {
139  ::fwGuiQt::container::QtContainer::sptr qtContainer = fwGuiQt::container::QtContainer::dynamicCast(getContainer());
140 
141  QHBoxLayout* layout = new QHBoxLayout();
142 
143  ::fwMedData::DicomSeries::csptr dicomSeries = this->getInOut< ::fwMedData::DicomSeries >("series");
144  SLM_ASSERT("DicomSeries should not be null !", dicomSeries);
145  m_numberOfSlices = dicomSeries->getNumberOfInstances();
146 
147  // Slider
148  m_sliceIndexSlider = new QSlider(Qt::Horizontal);
149  layout->addWidget(m_sliceIndexSlider, 1);
150  m_sliceIndexSlider->setRange(0, static_cast<int>(m_numberOfSlices-1));
151  m_sliceIndexSlider->setValue(static_cast<int>(m_numberOfSlices/2));
152 
153  // Line Edit
154  m_sliceIndexLineEdit = new QLineEdit();
155  layout->addWidget(m_sliceIndexLineEdit, 0);
156  m_sliceIndexLineEdit->setReadOnly(true);
157  m_sliceIndexLineEdit->setMaximumWidth(80);
158 
159  std::stringstream ss;
160  ss << m_sliceIndexSlider->value() << " / " << (m_numberOfSlices-1);
161  m_sliceIndexLineEdit->setText(std::string(ss.str()).c_str());
162 
163  qtContainer->setLayout(layout);
164 
165  // Connect the signals
166  QObject::connect(m_sliceIndexSlider, SIGNAL(valueChanged(int)), this, SLOT(changeSliceIndex(int)));
167 
168  // Create temporary SeriesDB
169  m_tempSeriesDB = ::fwMedData::SeriesDB::New();
170 
171  // Create reader
172  ::fwServices::registry::ServiceFactory::sptr srvFactory = ::fwServices::registry::ServiceFactory::getDefault();
173 
174  ::fwIO::IReader::sptr dicomReader;
175  dicomReader = ::fwIO::IReader::dynamicCast(srvFactory->create(m_dicomReaderType));
176  SLM_ASSERT("Unable to create a reader of type: \"" + m_dicomReaderType + "\" in "
177  "::ioDicomWeb::SSliceIndexDicomPullerEditor.", dicomReader);
178  ::fwServices::OSR::registerService(m_tempSeriesDB, ::fwIO::s_DATA_KEY,
179  ::fwServices::IService::AccessType::INOUT, dicomReader);
180  if(m_readerConfig)
181  {
182  dicomReader->setConfiguration(m_readerConfig);
183  }
184 
185  dicomReader->configure();
186  dicomReader->start();
187 
188  m_dicomReader = dicomReader;
189 
190  // Image Indecies
191  m_axialIndex = ::fwData::Integer::New(0);
192  m_frontalIndex = ::fwData::Integer::New(0);
193  m_sagittalIndex = ::fwData::Integer::New(0);
194 
195  // Load a slice
196  if(m_delayTimer)
197  {
198  if(m_delayTimer->isRunning())
199  {
200  m_delayTimer->stop();
201  }
202 
203  m_delayTimer->setDuration(std::chrono::milliseconds(m_delay));
204  m_delayTimer->start();
205  }
206  else
207  {
208  this->triggerNewSlice();
209  }
210 }
211 
212 //------------------------------------------------------------------------------
213 
215 {
216  if(m_delayTimer && m_delayTimer->isRunning())
217  {
218  m_delayTimer->stop();
219  }
220 
221  // Stop dicom reader
222  if(!m_dicomReader.expired())
223  {
224  m_dicomReader.lock()->stop();
225  ::fwServices::OSR::unregisterService(m_dicomReader.lock());
226  }
227 
228  this->destroy();
229 }
230 
231 //------------------------------------------------------------------------------
232 
234 {
235 }
236 
237 //------------------------------------------------------------------------------
238 
239 void SSliceIndexDicomPullerEditor::changeSliceIndex(int)
240 {
241  // Update text
242  std::stringstream ss;
243  ss << m_sliceIndexSlider->value() << " / " << (m_numberOfSlices-1);
244  m_sliceIndexLineEdit->setText(std::string(ss.str()).c_str());
245 
246  // Get the new slice if there is no change for m_delay milliseconds
247  if( m_delayTimer )
248  {
249  if(m_delayTimer->isRunning())
250  {
251  m_delayTimer->stop();
252  }
253 
254  m_delayTimer->start();
255  }
256  else
257  {
258  this->triggerNewSlice();
259  }
260 }
261 
262 //------------------------------------------------------------------------------
263 
264 void SSliceIndexDicomPullerEditor::triggerNewSlice()
265 {
266  // DicomSeries
267  ::fwMedData::DicomSeries::csptr dicomSeries = this->getInOut< ::fwMedData::DicomSeries >("series");
268  SLM_ASSERT("DicomSeries should not be null !", dicomSeries);
269 
270  // Compute slice index
271  const size_t selectedSliceIndex = static_cast<size_t>(m_sliceIndexSlider->value()) +
272  dicomSeries->getFirstInstanceNumber();
273  OSLM_TRACE("triggered new slice : " << selectedSliceIndex);
274  if(!dicomSeries->isInstanceAvailable(selectedSliceIndex))
275  {
276  this->pullInstance();
277  }
278  else
279  {
280  this->readImage(selectedSliceIndex);
281  }
282 }
283 
284 //------------------------------------------------------------------------------
285 
286 void SSliceIndexDicomPullerEditor::readImage(size_t selectedSliceIndex)
287 {
288  // DicomSeries
289  ::fwMedData::DicomSeries::csptr dicomSeries = this->getInOut< ::fwMedData::DicomSeries >("series");
290  SLM_ASSERT("DicomSeries should not be null !", dicomSeries);
291  if( dicomSeries->getModality() != "CT" && dicomSeries->getModality() != "MR" && dicomSeries->getModality() != "XA")
292  {
293  return;
294  }
295 
296  // Creates unique temporary folder, no need to check if exists before (see ::fwTools::System::getTemporaryFolder)
297  ::boost::filesystem::path path = ::fwTools::System::getTemporaryFolder("dicom");
298  ::boost::filesystem::path tmpPath = path / "tmp";
299 
300  SLM_INFO("Create " + tmpPath.string());
301  ::boost::filesystem::create_directories(tmpPath);
302 
303  const auto& binaries = dicomSeries->getDicomContainer();
304  auto iter = binaries.find(selectedSliceIndex);
305  OSLM_ASSERT("Index '"<<selectedSliceIndex<<"' is not found in DicomSeries", iter != binaries.end());
306 
307  const ::fwMemory::BufferObject::sptr bufferObj = iter->second;
308  const ::fwMemory::BufferObject::Lock lockerDest(bufferObj);
309  const char* buffer = static_cast<char*>(lockerDest.getBuffer());
310  const size_t size = bufferObj->getSize();
311 
312  ::boost::filesystem::path dest = tmpPath / std::to_string(selectedSliceIndex);
313  ::boost::filesystem::ofstream fs(dest, std::ios::binary|std::ios::trunc);
314  FW_RAISE_IF("Can't open '" << tmpPath << "' for write.", !fs.good());
315 
316  fs.write(buffer, size);
317  fs.close();
318 
319  // Read image
320  m_dicomReader.lock()->setFolder(tmpPath);
321  if(!m_dicomReader.expired())
322  {
323  m_dicomReader.lock()->update();
324 
325  if(m_dicomReader.expired() || m_dicomReader.lock()->isStopped())
326  {
327  return;
328  }
329  }
330  else
331  {
332  return;
333  }
334 
335  //Copy image
336  ::fwMedData::ImageSeries::sptr imageSeries;
337 
338  if(m_tempSeriesDB->getContainer().size() > 0)
339  {
340  imageSeries = ::fwMedData::ImageSeries::dynamicCast(*(m_tempSeriesDB->getContainer().begin()));
341  }
342 
343  if(imageSeries)
344  {
345  ::fwData::Image::sptr newImage = imageSeries->getImage();
346  ::fwData::Image::SizeType newSize = newImage->getSize();
347 
348  newImage->setField(::fwDataTools::fieldHelper::Image::m_axialSliceIndexId, m_axialIndex);
349  m_frontalIndex->setValue(static_cast<int>(newSize[0]/2));
350  newImage->setField(::fwDataTools::fieldHelper::Image::m_frontalSliceIndexId, m_frontalIndex);
351  m_sagittalIndex->setValue(static_cast<int>(newSize[1]/2));
352  newImage->setField(::fwDataTools::fieldHelper::Image::m_sagittalSliceIndexId, m_sagittalIndex);
353 
354  this->setOutput("image", newImage);
355  }
356 
357  ::boost::system::error_code ec;
358  ::boost::filesystem::remove_all(path, ec);
359  SLM_ERROR_IF("remove_all error for path " + path.string() + ": " + ec.message(), ec.value());
360 }
361 
362 //------------------------------------------------------------------------------
363 
364 void SSliceIndexDicomPullerEditor::pullInstance()
365 {
366  ::fwServices::IService::ConfigType configuration = this->getConfigTree();
367  //Parse server port and hostname
368  if(configuration.count("server"))
369  {
370  const std::string serverInfo = configuration.get("server", "");
371  const std::string::size_type splitPosition = serverInfo.find(':');
372  SLM_ASSERT("Server info not formatted correctly", splitPosition != std::string::npos);
373 
374  const std::string hostnameStr = serverInfo.substr(0, splitPosition);
375  const std::string portStr = serverInfo.substr(splitPosition + 1, serverInfo.size());
376 
377  m_serverHostnameKey = this->getPreferenceKey(hostnameStr);
378  m_serverPortKey = this->getPreferenceKey(portStr);
379  if(m_serverHostnameKey.empty())
380  {
381  m_serverHostname = hostnameStr;
382  }
383  if(m_serverPortKey.empty())
384  {
385  m_serverPort = std::stoi(portStr);
386  }
387  }
388  else
389  {
390  throw ::fwTools::Failed("'server' element not found");
391  }
392 
393  if(!m_serverHostnameKey.empty())
394  {
395  const std::string hostname = ::fwPreferences::getPreference(m_serverHostnameKey);
396  if(!hostname.empty())
397  {
398  m_serverHostname = hostname;
399  }
400  }
401  if(!m_serverPortKey.empty())
402  {
403  const std::string port = ::fwPreferences::getPreference(m_serverPortKey);
404  if(!port.empty())
405  {
406  m_serverPort = std::stoi(port);
407  }
408  }
409 
410  // Catch any errors
411  try
412  {
413  // DicomSeries
414  ::fwMedData::DicomSeries::sptr dicomSeries = this->getInOut< ::fwMedData::DicomSeries >("series");
415  SLM_ASSERT("DicomSeries should not be null !", dicomSeries);
416 
417  // Get selected slice
418  size_t selectedSliceIndex = static_cast<size_t>(m_sliceIndexSlider->value()) +
419  dicomSeries->getFirstInstanceNumber();
420 
421  std::string seriesInstanceUID = dicomSeries->getInstanceUID();
422 
423  // Find Series according to SeriesInstanceUID
424  QJsonObject query;
425  query.insert("SeriesInstanceUID", seriesInstanceUID.c_str());
426 
427  QJsonObject body;
428  body.insert("Level", "Series");
429  body.insert("Query", query);
430  body.insert("Limit", 0);
431 
433  const std::string pacsServer("http://" + m_serverHostname + ":" + std::to_string(m_serverPort));
434 
436  ::fwNetworkIO::http::Request::sptr request = ::fwNetworkIO::http::Request::New(
437  pacsServer + "/tools/find");
438  QByteArray seriesAnswer;
439  try
440  {
441  seriesAnswer = m_clientQt.post(request, QJsonDocument(body).toJson());
442  }
443  catch (::fwNetworkIO::exceptions::HostNotFound& exception)
444  {
445  std::stringstream ss;
446  ss << "Host not found:\n"
447  << " Please check your configuration: \n"
448  << "Pacs host name: " << m_serverHostname << "\n"
449  << "Pacs port: " << m_serverPort << "\n";
450 
451  this->displayErrorMessage(ss.str());
452  SLM_WARN(exception.what());
453  }
454  QJsonDocument jsonResponse = QJsonDocument::fromJson(seriesAnswer);
455  const QJsonArray& seriesArray = jsonResponse.array();
456 
457  // Should be one Series, so take the first of the array.
458  const std::string& seriesUID = seriesArray.at(0).toString().toStdString();
459  // GET all Instances by Series.
460  const std::string& instancesUrl(pacsServer + "/series/" + seriesUID);
461 
462  const QByteArray& instancesAnswer =
463  m_clientQt.get( ::fwNetworkIO::http::Request::New(instancesUrl));
464  jsonResponse = QJsonDocument::fromJson(instancesAnswer);
465  const QJsonObject& jsonObj = jsonResponse.object();
466  const QJsonArray& instancesArray = jsonObj["Instances"].toArray();
467  const std::string& instanceUID =
468  instancesArray.at(static_cast<int>(selectedSliceIndex)).toString().toStdString();
469 
470  // GET frame by Slice.
471  std::string instancePath;
472  const std::string& instanceUrl(pacsServer + "/instances/" + instanceUID + "/file");
473  try
474  {
475  instancePath = m_clientQt.getFile( ::fwNetworkIO::http::Request::New(instanceUrl));
476  }
477  catch (::fwNetworkIO::exceptions::ContentNotFound& exception)
478  {
479  std::stringstream ss;
480  ss << "Content not found: \n"
481  << "Unable download the DICOM instance. \n";
482 
483  this->displayErrorMessage(ss.str());
484  SLM_WARN(exception.what());
485  }
486 
487  // Add path and trigger reading
488  dicomSeries->addDicomPath(selectedSliceIndex, instancePath);
489  this->readImage(selectedSliceIndex);
490  }
491  catch (::fwNetworkIO::exceptions::Base& exception)
492  {
493  std::stringstream ss;
494  ss << "Unknown error.";
495  this->displayErrorMessage(ss.str());
496  SLM_WARN(exception.what());
497  }
498 }
499 
500 //------------------------------------------------------------------------------
501 
502 void SSliceIndexDicomPullerEditor::displayErrorMessage(const std::string& message) const
503 {
504  SLM_WARN("Error: " + message);
506  messageBox.setTitle("Error");
507  messageBox.setMessage( message );
508  messageBox.setIcon(::fwGui::dialog::IMessageDialog::CRITICAL);
509  messageBox.addButton(::fwGui::dialog::IMessageDialog::OK);
510  messageBox.show();
511 }
512 
513 } // namespace ioDicomWeb
FWNETWORKIO_API QByteArray post(Request::sptr request, const QByteArray &body)
Performs POST request.
Definition: ClientQt.cpp:124
static FWNETWORKIO_API Request::sptr New(const std::string &url)
Creates a new Request with given url.
Definition: Request.cpp:26
#define OSLM_ASSERT(message, cond)
work like &#39;assert&#39; from &#39;cassert&#39;, with in addition a message logged by spylog (with FATAL loglevel) ...
Definition: spyLog.hpp:310
virtual FWGUI_API void setMessage(const std::string &msg) override
Set the message.
Defines the generic message box for IHM. Use the Delegate design pattern.
FWGUI_API void destroy()
Stops sub-views and toobar services. Destroys view, sub-views and toolbar containers.
FWNETWORKIO_API std::string getFile(Request::sptr request)
Retrieves data over network.
Definition: ClientQt.cpp:58
#define OSLM_TRACE(message)
Definition: spyLog.hpp:230
#define SLM_WARN(message)
Definition: spyLog.hpp:261
FWSERVICES_API void setOutput(const ::fwServices::IService::KeyType &key, const ::fwData::Object::sptr &object, size_t index=0)
Register an output object at a given key in the OSR, replacing it if it already exists.
Definition: IService.cpp:80
virtual FWGUI_API void addButton(IMessageDialog::Buttons button) override
Add a button (OK, YES_NO, YES, NO, CANCEL)
FWNETWORKIO_API QByteArray get(Request::sptr request)
Retrieves data over network.
Definition: ClientQt.cpp:36
virtual FWGUI_API IMessageDialog::Buttons show() override
Show the message box and return the clicked button.
#define SLM_ERROR_IF(message, cond)
Definition: spyLog.hpp:276
#define SLM_ASSERT(message, cond)
work like &#39;assert&#39; from &#39;cassert&#39;, with in addition a message logged by spylog (with FATAL loglevel) ...
Definition: spyLog.hpp:308
::fwRuntime::ConfigurationElement::sptr m_configuration
Configuration element used to configure service internal state using a generic XML like structure TOD...
Definition: IService.hpp:670
static FWTOOLS_APIconst::boost::filesystem::path getTemporaryFolder(const std::string &subFolderPrefix="") noexcept
Returns a unique per-process temporary folder. The top level temporary folder will be automatically d...
Definition: System.cpp:148
FWGUI_API void create()
Creates view, sub-views and toolbar containers. Manages sub-views and toobar services.
virtual FWGUI_API void setIcon(IMessageDialog::Icons icon) override
Set the icon (CRITICAL, WARNING, INFO or QUESTION)
virtual IODICOMWEB_API void starting() override
Creates the wigdets, connect the signal, register the DICOM reader and starts the callback timer...
Implements exception for HTTP content not found errors.
Implements exception for an HTTP host not found errors.
virtual IODICOMWEB_API void stopping() override
Stops the timer, unregister the DICOM reader and destroy the created widgets.
ioDicomWeb contains services use to deal with PACS through HTTP.
static FWSERVICES_API ServiceFactory::sptr getDefault()
Return the unique Instance, create it if required at first access.
std::shared_ptr< ::fwThread::Worker > m_associatedWorker
Associated worker.
Definition: IService.hpp:699
#define SLM_INFO(message)
Definition: spyLog.hpp:250
virtual IODICOMWEB_API void configuring() override
Gets the configurations and creates a timer on a worker.
::fwData::Array::SizeType SizeType
Image size type.
virtual FWGUI_API void setTitle(const std::string &title) override
Set the title of the message box.
FWGUI_API void initialize()
Initialize managers.
FWSERVICES_API ConfigType getConfigTree() const
Return the configuration, in an boost property tree.
Definition: IService.cpp:247