820
|
1 /**
|
|
2 * Stone of Orthanc
|
|
3 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
|
|
4 * Department, University Hospital of Liege, Belgium
|
|
5 * Copyright (C) 2017-2019 Osimis S.A., Belgium
|
|
6 *
|
|
7 * This program is free software: you can redistribute it and/or
|
|
8 * modify it under the terms of the GNU Affero General Public License
|
|
9 * as published by the Free Software Foundation, either version 3 of
|
|
10 * the License, or (at your option) any later version.
|
|
11 *
|
|
12 * This program is distributed in the hope that it will be useful, but
|
|
13 * WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
15 * Affero General Public License for more details.
|
|
16 *
|
|
17 * You should have received a copy of the GNU Affero General Public License
|
|
18 * along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
19 **/
|
|
20
|
|
21
|
|
22
|
|
23 #include <emscripten.h>
|
|
24 #include <emscripten/fetch.h>
|
|
25 #include <emscripten/html5.h>
|
|
26
|
|
27 #include "../../Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h"
|
|
28 #include "../../Framework/OpenGL/WebAssemblyOpenGLContext.h"
|
|
29 #include "../../Framework/Scene2D/GrayscaleStyleConfigurator.h"
|
|
30 #include "../../Framework/Scene2D/OpenGLCompositor.h"
|
|
31 #include "../../Framework/Scene2D/PanSceneTracker.h"
|
|
32 #include "../../Framework/Scene2D/RotateSceneTracker.h"
|
|
33 #include "../../Framework/Scene2D/ZoomSceneTracker.h"
|
|
34 #include "../../Framework/Scene2DViewport/IFlexiblePointerTracker.h"
|
|
35 #include "../../Framework/Scene2DViewport/ViewportController.h"
|
|
36 #include "../../Framework/StoneInitialization.h"
|
|
37 #include "../../Framework/Volumes/VolumeSceneLayerSource.h"
|
|
38
|
|
39 #include <Core/OrthancException.h>
|
|
40 #include <Core/Toolbox.h>
|
|
41
|
|
42
|
|
43 static const unsigned int FONT_SIZE = 32;
|
|
44
|
|
45
|
|
46 namespace OrthancStone
|
|
47 {
|
|
48 class WebAssemblyViewport : public boost::noncopyable
|
|
49 {
|
|
50 private:
|
|
51 // the construction order is important because compositor_
|
|
52 // will hold a reference to the scene that belong to the
|
|
53 // controller_ object
|
|
54 OpenGL::WebAssemblyOpenGLContext context_;
|
|
55 boost::shared_ptr<ViewportController> controller_;
|
|
56 OpenGLCompositor compositor_;
|
|
57
|
|
58 void SetupEvents(const std::string& canvas);
|
|
59
|
|
60 public:
|
|
61 WebAssemblyViewport(MessageBroker& broker,
|
|
62 const std::string& canvas) :
|
|
63 context_(canvas),
|
|
64 controller_(new ViewportController(broker)),
|
|
65 compositor_(context_, *controller_->GetScene())
|
|
66 {
|
|
67 compositor_.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT,
|
|
68 FONT_SIZE, Orthanc::Encoding_Latin1);
|
|
69 SetupEvents(canvas);
|
|
70 }
|
|
71
|
|
72 Scene2D& GetScene()
|
|
73 {
|
|
74 return *controller_->GetScene();
|
|
75 }
|
|
76
|
|
77 const boost::shared_ptr<ViewportController>& GetController()
|
|
78 {
|
|
79 return controller_;
|
|
80 }
|
|
81
|
|
82 void UpdateSize()
|
|
83 {
|
|
84 context_.UpdateSize();
|
|
85 compositor_.UpdateSize();
|
|
86 Refresh();
|
|
87 }
|
|
88
|
|
89 void Refresh()
|
|
90 {
|
|
91 compositor_.Refresh();
|
|
92 }
|
|
93
|
822
|
94 void FitContent()
|
|
95 {
|
|
96 GetScene().FitContent(context_.GetCanvasWidth(), context_.GetCanvasHeight());
|
|
97 }
|
|
98
|
820
|
99 const std::string& GetCanvasIdentifier() const
|
|
100 {
|
|
101 return context_.GetCanvasIdentifier();
|
|
102 }
|
|
103
|
|
104 ScenePoint2D GetPixelCenterCoordinates(int x, int y) const
|
|
105 {
|
|
106 return compositor_.GetPixelCenterCoordinates(x, y);
|
|
107 }
|
|
108
|
|
109 unsigned int GetCanvasWidth() const
|
|
110 {
|
|
111 return context_.GetCanvasWidth();
|
|
112 }
|
|
113
|
|
114 unsigned int GetCanvasHeight() const
|
|
115 {
|
|
116 return context_.GetCanvasHeight();
|
|
117 }
|
|
118 };
|
|
119
|
|
120 class ActiveTracker : public boost::noncopyable
|
|
121 {
|
|
122 private:
|
|
123 boost::shared_ptr<IFlexiblePointerTracker> tracker_;
|
|
124 std::string canvasIdentifier_;
|
|
125 bool insideCanvas_;
|
|
126
|
|
127 public:
|
|
128 ActiveTracker(const boost::shared_ptr<IFlexiblePointerTracker>& tracker,
|
|
129 const WebAssemblyViewport& viewport) :
|
|
130 tracker_(tracker),
|
|
131 canvasIdentifier_(viewport.GetCanvasIdentifier()),
|
|
132 insideCanvas_(true)
|
|
133 {
|
|
134 if (tracker_.get() == NULL)
|
|
135 {
|
|
136 throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
|
|
137 }
|
|
138 }
|
|
139
|
|
140 bool IsAlive() const
|
|
141 {
|
|
142 return tracker_->IsAlive();
|
|
143 }
|
|
144
|
|
145 void PointerMove(const PointerEvent& event)
|
|
146 {
|
|
147 tracker_->PointerMove(event);
|
|
148 }
|
|
149
|
|
150 void PointerUp(const PointerEvent& event)
|
|
151 {
|
|
152 tracker_->PointerUp(event);
|
|
153 }
|
|
154 };
|
|
155 }
|
|
156
|
|
157 static OrthancStone::PointerEvent* ConvertMouseEvent(
|
|
158 const EmscriptenMouseEvent& source,
|
|
159 OrthancStone::WebAssemblyViewport& viewport)
|
|
160 {
|
|
161 std::auto_ptr<OrthancStone::PointerEvent> target(
|
|
162 new OrthancStone::PointerEvent);
|
|
163
|
|
164 target->AddPosition(viewport.GetPixelCenterCoordinates(
|
|
165 source.targetX, source.targetY));
|
|
166 target->SetAltModifier(source.altKey);
|
|
167 target->SetControlModifier(source.ctrlKey);
|
|
168 target->SetShiftModifier(source.shiftKey);
|
|
169
|
|
170 return target.release();
|
|
171 }
|
|
172
|
|
173 std::auto_ptr<OrthancStone::ActiveTracker> tracker_;
|
|
174
|
|
175 EM_BOOL OnMouseEvent(int eventType,
|
|
176 const EmscriptenMouseEvent *mouseEvent,
|
|
177 void *userData)
|
|
178 {
|
|
179 if (mouseEvent != NULL &&
|
|
180 userData != NULL)
|
|
181 {
|
|
182 OrthancStone::WebAssemblyViewport& viewport =
|
|
183 *reinterpret_cast<OrthancStone::WebAssemblyViewport*>(userData);
|
|
184
|
|
185 switch (eventType)
|
|
186 {
|
|
187 case EMSCRIPTEN_EVENT_CLICK:
|
|
188 {
|
|
189 static unsigned int count = 0;
|
|
190 char buf[64];
|
|
191 sprintf(buf, "click %d", count++);
|
|
192
|
|
193 std::auto_ptr<OrthancStone::TextSceneLayer> layer(new OrthancStone::TextSceneLayer);
|
|
194 layer->SetText(buf);
|
|
195 viewport.GetScene().SetLayer(100, layer.release());
|
|
196 viewport.Refresh();
|
|
197 break;
|
|
198 }
|
|
199
|
|
200 case EMSCRIPTEN_EVENT_MOUSEDOWN:
|
|
201 {
|
|
202 boost::shared_ptr<OrthancStone::IFlexiblePointerTracker> t;
|
|
203
|
|
204 {
|
|
205 std::auto_ptr<OrthancStone::PointerEvent> event(
|
|
206 ConvertMouseEvent(*mouseEvent, viewport));
|
|
207
|
|
208 switch (mouseEvent->button)
|
|
209 {
|
|
210 case 0: // Left button
|
|
211 emscripten_console_log("Creating RotateSceneTracker");
|
|
212 t.reset(new OrthancStone::RotateSceneTracker(
|
|
213 viewport.GetController(), *event));
|
|
214 break;
|
|
215
|
|
216 case 1: // Middle button
|
|
217 emscripten_console_log("Creating PanSceneTracker");
|
|
218 LOG(INFO) << "Creating PanSceneTracker" ;
|
|
219 t.reset(new OrthancStone::PanSceneTracker(
|
|
220 viewport.GetController(), *event));
|
|
221 break;
|
|
222
|
|
223 case 2: // Right button
|
|
224 emscripten_console_log("Creating ZoomSceneTracker");
|
|
225 t.reset(new OrthancStone::ZoomSceneTracker(
|
|
226 viewport.GetController(), *event, viewport.GetCanvasWidth()));
|
|
227 break;
|
|
228
|
|
229 default:
|
|
230 break;
|
|
231 }
|
|
232 }
|
|
233
|
|
234 if (t.get() != NULL)
|
|
235 {
|
|
236 tracker_.reset(
|
|
237 new OrthancStone::ActiveTracker(t, viewport));
|
|
238 viewport.Refresh();
|
|
239 }
|
|
240
|
|
241 break;
|
|
242 }
|
|
243
|
|
244 case EMSCRIPTEN_EVENT_MOUSEMOVE:
|
|
245 if (tracker_.get() != NULL)
|
|
246 {
|
|
247 std::auto_ptr<OrthancStone::PointerEvent> event(
|
|
248 ConvertMouseEvent(*mouseEvent, viewport));
|
|
249 tracker_->PointerMove(*event);
|
|
250 viewport.Refresh();
|
|
251 }
|
|
252 break;
|
|
253
|
|
254 case EMSCRIPTEN_EVENT_MOUSEUP:
|
|
255 if (tracker_.get() != NULL)
|
|
256 {
|
|
257 std::auto_ptr<OrthancStone::PointerEvent> event(
|
|
258 ConvertMouseEvent(*mouseEvent, viewport));
|
|
259 tracker_->PointerUp(*event);
|
|
260 viewport.Refresh();
|
|
261 if (!tracker_->IsAlive())
|
|
262 tracker_.reset();
|
|
263 }
|
|
264 break;
|
|
265
|
|
266 default:
|
|
267 break;
|
|
268 }
|
|
269 }
|
|
270
|
|
271 return true;
|
|
272 }
|
|
273
|
|
274
|
|
275 void OrthancStone::WebAssemblyViewport::SetupEvents(const std::string& canvas)
|
|
276 {
|
|
277 emscripten_set_mousedown_callback(canvas.c_str(), this, false, OnMouseEvent);
|
|
278 emscripten_set_mousemove_callback(canvas.c_str(), this, false, OnMouseEvent);
|
|
279 emscripten_set_mouseup_callback(canvas.c_str(), this, false, OnMouseEvent);
|
|
280 }
|
|
281
|
|
282
|
|
283
|
|
284
|
|
285 namespace OrthancStone
|
|
286 {
|
|
287 class VolumeSlicerViewport : public IObserver
|
|
288 {
|
|
289 private:
|
|
290 OrthancStone::WebAssemblyViewport viewport_;
|
|
291 std::auto_ptr<VolumeSceneLayerSource> source_;
|
|
292 VolumeProjection projection_;
|
|
293 std::vector<CoordinateSystem3D> planes_;
|
|
294 size_t currentPlane_;
|
|
295
|
|
296 void Handle(const DicomVolumeImage::GeometryReadyMessage& message)
|
|
297 {
|
|
298 LOG(INFO) << "Geometry is available";
|
|
299
|
|
300 const VolumeImageGeometry& geometry = message.GetOrigin().GetGeometry();
|
|
301
|
|
302 const unsigned int depth = geometry.GetProjectionDepth(projection_);
|
|
303 currentPlane_ = depth / 2;
|
|
304
|
|
305 planes_.resize(depth);
|
|
306
|
|
307 for (unsigned int z = 0; z < depth; z++)
|
|
308 {
|
|
309 planes_[z] = geometry.GetProjectionSlice(projection_, z);
|
|
310 }
|
822
|
311
|
|
312 Refresh();
|
|
313
|
|
314 viewport_.FitContent();
|
820
|
315 }
|
|
316
|
|
317 public:
|
|
318 VolumeSlicerViewport(MessageBroker& broker,
|
|
319 const std::string& canvas,
|
|
320 VolumeProjection projection) :
|
|
321 IObserver(broker),
|
|
322 viewport_(broker, canvas),
|
|
323 projection_(projection),
|
|
324 currentPlane_(0)
|
|
325 {
|
|
326 }
|
|
327
|
|
328 void UpdateSize()
|
|
329 {
|
|
330 viewport_.UpdateSize();
|
|
331 }
|
|
332
|
|
333 void SetSlicer(int layerDepth,
|
|
334 const boost::shared_ptr<IVolumeSlicer>& slicer,
|
|
335 IObservable& loader,
|
|
336 ILayerStyleConfigurator* configurator)
|
|
337 {
|
|
338 if (source_.get() != NULL)
|
|
339 {
|
|
340 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls,
|
|
341 "Only one slicer can be registered");
|
|
342 }
|
|
343
|
|
344 loader.RegisterObserverCallback(
|
|
345 new Callable<VolumeSlicerViewport, DicomVolumeImage::GeometryReadyMessage>
|
|
346 (*this, &VolumeSlicerViewport::Handle));
|
|
347
|
|
348 source_.reset(new VolumeSceneLayerSource(viewport_.GetScene(), layerDepth, slicer));
|
|
349
|
|
350 if (configurator != NULL)
|
|
351 {
|
|
352 source_->SetConfigurator(configurator);
|
|
353 }
|
|
354 }
|
|
355
|
|
356 void Refresh()
|
|
357 {
|
|
358 if (source_.get() != NULL &&
|
|
359 currentPlane_ < planes_.size())
|
|
360 {
|
|
361 source_->Update(planes_[currentPlane_]);
|
822
|
362 viewport_.Refresh();
|
820
|
363 }
|
|
364 }
|
823
|
365
|
|
366 void Scroll(int delta)
|
|
367 {
|
|
368 if (!planes_.empty())
|
|
369 {
|
|
370 int tmp = static_cast<int>(currentPlane_) + delta;
|
|
371 unsigned int next;
|
|
372
|
|
373 if (tmp < 0)
|
|
374 {
|
|
375 next = 0;
|
|
376 }
|
|
377 else if (tmp >= static_cast<int>(planes_.size()))
|
|
378 {
|
|
379 next = planes_.size() - 1;
|
|
380 }
|
|
381 else
|
|
382 {
|
|
383 next = static_cast<size_t>(tmp);
|
|
384 }
|
|
385
|
|
386 if (next != currentPlane_)
|
|
387 {
|
|
388 currentPlane_ = next;
|
|
389 Refresh();
|
|
390 }
|
|
391 }
|
|
392 }
|
820
|
393 };
|
|
394
|
|
395
|
|
396
|
|
397
|
|
398 class WebAssemblyOracle :
|
|
399 public IOracle,
|
|
400 public IObservable
|
|
401 {
|
|
402 private:
|
|
403 typedef std::map<std::string, std::string> HttpHeaders;
|
|
404
|
|
405 class FetchContext : public boost::noncopyable
|
|
406 {
|
|
407 private:
|
|
408 class Emitter : public IMessageEmitter
|
|
409 {
|
|
410 private:
|
|
411 WebAssemblyOracle& oracle_;
|
|
412
|
|
413 public:
|
|
414 Emitter(WebAssemblyOracle& oracle) :
|
|
415 oracle_(oracle)
|
|
416 {
|
|
417 }
|
|
418
|
|
419 virtual void EmitMessage(const IObserver& receiver,
|
|
420 const IMessage& message)
|
|
421 {
|
|
422 oracle_.EmitMessage(receiver, message);
|
|
423 }
|
|
424 };
|
|
425
|
|
426 Emitter emitter_;
|
|
427 const IObserver& receiver_;
|
|
428 std::auto_ptr<IOracleCommand> command_;
|
|
429 std::string expectedContentType_;
|
|
430
|
|
431 public:
|
|
432 FetchContext(WebAssemblyOracle& oracle,
|
|
433 const IObserver& receiver,
|
|
434 IOracleCommand* command,
|
|
435 const std::string& expectedContentType) :
|
|
436 emitter_(oracle),
|
|
437 receiver_(receiver),
|
|
438 command_(command),
|
|
439 expectedContentType_(expectedContentType)
|
|
440 {
|
|
441 if (command == NULL)
|
|
442 {
|
|
443 throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
|
|
444 }
|
|
445 }
|
|
446
|
|
447 const std::string& GetExpectedContentType() const
|
|
448 {
|
|
449 return expectedContentType_;
|
|
450 }
|
|
451
|
|
452 void EmitMessage(const IMessage& message)
|
|
453 {
|
|
454 emitter_.EmitMessage(receiver_, message);
|
|
455 }
|
|
456
|
|
457 IMessageEmitter& GetEmitter()
|
|
458 {
|
|
459 return emitter_;
|
|
460 }
|
|
461
|
|
462 const IObserver& GetReceiver() const
|
|
463 {
|
|
464 return receiver_;
|
|
465 }
|
|
466
|
|
467 IOracleCommand& GetCommand() const
|
|
468 {
|
|
469 return *command_;
|
|
470 }
|
|
471
|
|
472 template <typename T>
|
|
473 const T& GetTypedCommand() const
|
|
474 {
|
|
475 return dynamic_cast<T&>(*command_);
|
|
476 }
|
|
477 };
|
|
478
|
|
479 static void FetchSucceeded(emscripten_fetch_t *fetch)
|
|
480 {
|
|
481 /**
|
|
482 * Firstly, make a local copy of the fetched information, and
|
|
483 * free data associated with the fetch.
|
|
484 **/
|
|
485
|
|
486 std::auto_ptr<FetchContext> context(reinterpret_cast<FetchContext*>(fetch->userData));
|
|
487
|
|
488 std::string answer;
|
|
489 if (fetch->numBytes > 0)
|
|
490 {
|
|
491 answer.assign(fetch->data, fetch->numBytes);
|
|
492 }
|
|
493
|
|
494 /**
|
|
495 * TODO - HACK - As of emscripten-1.38.31, the fetch API does
|
|
496 * not contain a way to retrieve the HTTP headers of the
|
|
497 * answer. We make the assumption that the "Content-Type" header
|
|
498 * of the response is the same as the "Accept" header of the
|
|
499 * query. This should be fixed in future versions of emscripten.
|
|
500 * https://github.com/emscripten-core/emscripten/pull/8486
|
|
501 **/
|
|
502
|
|
503 HttpHeaders headers;
|
|
504 if (!context->GetExpectedContentType().empty())
|
|
505 {
|
|
506 headers["Content-Type"] = context->GetExpectedContentType();
|
|
507 }
|
|
508
|
|
509
|
|
510 emscripten_fetch_close(fetch);
|
|
511
|
|
512
|
|
513 /**
|
|
514 * Secondly, use the retrieved data.
|
|
515 **/
|
|
516
|
|
517 try
|
|
518 {
|
|
519 if (context.get() == NULL)
|
|
520 {
|
|
521 throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
|
|
522 }
|
|
523 else
|
|
524 {
|
|
525 switch (context->GetCommand().GetType())
|
|
526 {
|
|
527 case IOracleCommand::Type_OrthancRestApi:
|
|
528 {
|
|
529 OrthancRestApiCommand::SuccessMessage message
|
|
530 (context->GetTypedCommand<OrthancRestApiCommand>(), headers, answer);
|
|
531 context->EmitMessage(message);
|
|
532 break;
|
|
533 }
|
|
534
|
|
535 case IOracleCommand::Type_GetOrthancImage:
|
|
536 {
|
|
537 context->GetTypedCommand<GetOrthancImageCommand>().ProcessHttpAnswer
|
|
538 (context->GetEmitter(), context->GetReceiver(), answer, headers);
|
|
539 break;
|
|
540 }
|
|
541
|
|
542 case IOracleCommand::Type_GetOrthancWebViewerJpeg:
|
|
543 {
|
|
544 context->GetTypedCommand<GetOrthancWebViewerJpegCommand>().ProcessHttpAnswer
|
|
545 (context->GetEmitter(), context->GetReceiver(), answer);
|
|
546 break;
|
|
547 }
|
|
548
|
|
549 default:
|
|
550 LOG(ERROR) << "Command type not implemented by the WebAssembly Oracle: "
|
|
551 << context->GetCommand().GetType();
|
|
552 }
|
|
553 }
|
|
554 }
|
|
555 catch (Orthanc::OrthancException& e)
|
|
556 {
|
|
557 LOG(ERROR) << "Error while processing a fetch answer in the oracle: " << e.What();
|
|
558 }
|
|
559 }
|
|
560
|
|
561 static void FetchFailed(emscripten_fetch_t *fetch)
|
|
562 {
|
|
563 std::auto_ptr<FetchContext> context(reinterpret_cast<FetchContext*>(fetch->userData));
|
|
564
|
|
565 LOG(ERROR) << "Fetching " << fetch->url << " failed, HTTP failure status code: " << fetch->status;
|
|
566
|
|
567 /**
|
|
568 * TODO - The following code leads to an infinite recursion, at
|
|
569 * least with Firefox running on incognito mode => WHY?
|
|
570 **/
|
|
571 //emscripten_fetch_close(fetch); // Also free data on failure.
|
|
572 }
|
|
573
|
|
574
|
|
575 class FetchCommand : public boost::noncopyable
|
|
576 {
|
|
577 private:
|
|
578 WebAssemblyOracle& oracle_;
|
|
579 const IObserver& receiver_;
|
|
580 std::auto_ptr<IOracleCommand> command_;
|
|
581 Orthanc::HttpMethod method_;
|
|
582 std::string uri_;
|
|
583 std::string body_;
|
|
584 HttpHeaders headers_;
|
|
585 unsigned int timeout_;
|
|
586 std::string expectedContentType_;
|
|
587
|
|
588 public:
|
|
589 FetchCommand(WebAssemblyOracle& oracle,
|
|
590 const IObserver& receiver,
|
|
591 IOracleCommand* command) :
|
|
592 oracle_(oracle),
|
|
593 receiver_(receiver),
|
|
594 command_(command),
|
|
595 method_(Orthanc::HttpMethod_Get),
|
|
596 timeout_(0)
|
|
597 {
|
|
598 if (command == NULL)
|
|
599 {
|
|
600 throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
|
|
601 }
|
|
602 }
|
|
603
|
|
604 void SetMethod(Orthanc::HttpMethod method)
|
|
605 {
|
|
606 method_ = method;
|
|
607 }
|
|
608
|
|
609 void SetUri(const std::string& uri)
|
|
610 {
|
|
611 uri_ = uri;
|
|
612 }
|
|
613
|
|
614 void SetBody(std::string& body /* will be swapped */)
|
|
615 {
|
|
616 body_.swap(body);
|
|
617 }
|
|
618
|
|
619 void SetHttpHeaders(const HttpHeaders& headers)
|
|
620 {
|
|
621 headers_ = headers;
|
|
622 }
|
|
623
|
|
624 void SetTimeout(unsigned int timeout)
|
|
625 {
|
|
626 timeout_ = timeout;
|
|
627 }
|
|
628
|
|
629 void Execute()
|
|
630 {
|
|
631 if (command_.get() == NULL)
|
|
632 {
|
|
633 // Cannot call Execute() twice
|
|
634 throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
|
|
635 }
|
|
636
|
|
637 emscripten_fetch_attr_t attr;
|
|
638 emscripten_fetch_attr_init(&attr);
|
|
639
|
|
640 const char* method;
|
|
641
|
|
642 switch (method_)
|
|
643 {
|
|
644 case Orthanc::HttpMethod_Get:
|
|
645 method = "GET";
|
|
646 break;
|
|
647
|
|
648 case Orthanc::HttpMethod_Post:
|
|
649 method = "POST";
|
|
650 break;
|
|
651
|
|
652 case Orthanc::HttpMethod_Delete:
|
|
653 method = "DELETE";
|
|
654 break;
|
|
655
|
|
656 case Orthanc::HttpMethod_Put:
|
|
657 method = "PUT";
|
|
658 break;
|
|
659
|
|
660 default:
|
|
661 throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
|
|
662 }
|
|
663
|
|
664 strcpy(attr.requestMethod, method);
|
|
665
|
|
666 attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
|
|
667 attr.onsuccess = FetchSucceeded;
|
|
668 attr.onerror = FetchFailed;
|
|
669 attr.timeoutMSecs = timeout_ * 1000;
|
|
670
|
|
671 std::vector<const char*> headers;
|
|
672 headers.reserve(2 * headers_.size() + 1);
|
|
673
|
|
674 std::string expectedContentType;
|
|
675
|
|
676 for (HttpHeaders::const_iterator it = headers_.begin(); it != headers_.end(); ++it)
|
|
677 {
|
|
678 std::string key;
|
|
679 Orthanc::Toolbox::ToLowerCase(key, it->first);
|
|
680
|
|
681 if (key == "accept")
|
|
682 {
|
|
683 expectedContentType = it->second;
|
|
684 }
|
|
685
|
|
686 if (key != "accept-encoding") // Web browsers forbid the modification of this HTTP header
|
|
687 {
|
|
688 headers.push_back(it->first.c_str());
|
|
689 headers.push_back(it->second.c_str());
|
|
690 }
|
|
691 }
|
|
692
|
|
693 headers.push_back(NULL); // Termination of the array of HTTP headers
|
|
694
|
|
695 attr.requestHeaders = &headers[0];
|
|
696
|
|
697 if (!body_.empty())
|
|
698 {
|
|
699 attr.requestDataSize = body_.size();
|
|
700 attr.requestData = body_.c_str();
|
|
701 }
|
|
702
|
|
703 // Must be the last call to prevent memory leak on error
|
|
704 attr.userData = new FetchContext(oracle_, receiver_, command_.release(), expectedContentType);
|
|
705 emscripten_fetch(&attr, uri_.c_str());
|
|
706 }
|
|
707 };
|
|
708
|
|
709
|
|
710 void Execute(const IObserver& receiver,
|
|
711 OrthancRestApiCommand* command)
|
|
712 {
|
|
713 FetchCommand fetch(*this, receiver, command);
|
|
714
|
|
715 fetch.SetMethod(command->GetMethod());
|
|
716 fetch.SetUri(command->GetUri());
|
|
717 fetch.SetHttpHeaders(command->GetHttpHeaders());
|
|
718 fetch.SetTimeout(command->GetTimeout());
|
|
719
|
|
720 if (command->GetMethod() == Orthanc::HttpMethod_Put ||
|
|
721 command->GetMethod() == Orthanc::HttpMethod_Put)
|
|
722 {
|
|
723 std::string body;
|
|
724 command->SwapBody(body);
|
|
725 fetch.SetBody(body);
|
|
726 }
|
|
727
|
|
728 fetch.Execute();
|
|
729 }
|
|
730
|
|
731
|
|
732 void Execute(const IObserver& receiver,
|
|
733 GetOrthancImageCommand* command)
|
|
734 {
|
|
735 FetchCommand fetch(*this, receiver, command);
|
|
736
|
|
737 fetch.SetUri(command->GetUri());
|
|
738 fetch.SetHttpHeaders(command->GetHttpHeaders());
|
|
739 fetch.SetTimeout(command->GetTimeout());
|
|
740
|
|
741 fetch.Execute();
|
|
742 }
|
|
743
|
|
744
|
|
745 void Execute(const IObserver& receiver,
|
|
746 GetOrthancWebViewerJpegCommand* command)
|
|
747 {
|
|
748 FetchCommand fetch(*this, receiver, command);
|
|
749
|
|
750 fetch.SetUri(command->GetUri());
|
|
751 fetch.SetHttpHeaders(command->GetHttpHeaders());
|
|
752 fetch.SetTimeout(command->GetTimeout());
|
|
753
|
|
754 fetch.Execute();
|
|
755 }
|
|
756
|
|
757
|
|
758 public:
|
|
759 WebAssemblyOracle(MessageBroker& broker) :
|
|
760 IObservable(broker)
|
|
761 {
|
|
762 }
|
|
763
|
|
764 virtual void Schedule(const IObserver& receiver,
|
|
765 IOracleCommand* command)
|
|
766 {
|
|
767 std::auto_ptr<IOracleCommand> protection(command);
|
|
768
|
|
769 if (command == NULL)
|
|
770 {
|
|
771 throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
|
|
772 }
|
|
773
|
|
774 switch (command->GetType())
|
|
775 {
|
|
776 case IOracleCommand::Type_OrthancRestApi:
|
|
777 Execute(receiver, dynamic_cast<OrthancRestApiCommand*>(protection.release()));
|
|
778 break;
|
|
779
|
|
780 case IOracleCommand::Type_GetOrthancImage:
|
|
781 Execute(receiver, dynamic_cast<GetOrthancImageCommand*>(protection.release()));
|
|
782 break;
|
|
783
|
|
784 case IOracleCommand::Type_GetOrthancWebViewerJpeg:
|
|
785 Execute(receiver, dynamic_cast<GetOrthancWebViewerJpegCommand*>(protection.release()));
|
|
786 break;
|
|
787
|
|
788 default:
|
|
789 LOG(ERROR) << "Command type not implemented by the WebAssembly Oracle: " << command->GetType();
|
|
790 }
|
|
791 }
|
|
792
|
|
793 virtual void Start()
|
|
794 {
|
|
795 }
|
|
796
|
|
797 virtual void Stop()
|
|
798 {
|
|
799 }
|
|
800 };
|
|
801 }
|
|
802
|
|
803
|
|
804
|
|
805
|
|
806 boost::shared_ptr<OrthancStone::DicomVolumeImage> ct_(new OrthancStone::DicomVolumeImage);
|
|
807
|
|
808 boost::shared_ptr<OrthancStone::OrthancSeriesVolumeProgressiveLoader> loader_;
|
|
809
|
|
810 std::auto_ptr<OrthancStone::VolumeSlicerViewport> viewport1_;
|
|
811 std::auto_ptr<OrthancStone::VolumeSlicerViewport> viewport2_;
|
|
812 std::auto_ptr<OrthancStone::VolumeSlicerViewport> viewport3_;
|
|
813
|
|
814 OrthancStone::MessageBroker broker_;
|
|
815 OrthancStone::WebAssemblyOracle oracle_(broker_);
|
|
816
|
|
817
|
|
818 EM_BOOL OnWindowResize(int eventType, const EmscriptenUiEvent *uiEvent, void *userData)
|
|
819 {
|
|
820 try
|
|
821 {
|
|
822 if (viewport1_.get() != NULL)
|
|
823 {
|
|
824 viewport1_->UpdateSize();
|
|
825 }
|
|
826
|
|
827 if (viewport2_.get() != NULL)
|
|
828 {
|
|
829 viewport2_->UpdateSize();
|
|
830 }
|
|
831
|
|
832 if (viewport3_.get() != NULL)
|
|
833 {
|
|
834 viewport3_->UpdateSize();
|
|
835 }
|
|
836 }
|
|
837 catch (Orthanc::OrthancException& e)
|
|
838 {
|
|
839 LOG(ERROR) << "Exception while updating canvas size: " << e.What();
|
|
840 }
|
|
841
|
|
842 return true;
|
|
843 }
|
|
844
|
|
845
|
|
846
|
|
847
|
|
848 EM_BOOL OnAnimationFrame(double time, void *userData)
|
|
849 {
|
|
850 try
|
|
851 {
|
|
852 if (viewport1_.get() != NULL)
|
|
853 {
|
|
854 viewport1_->Refresh();
|
|
855 }
|
|
856
|
|
857 if (viewport2_.get() != NULL)
|
|
858 {
|
|
859 viewport2_->Refresh();
|
|
860 }
|
|
861
|
|
862 if (viewport3_.get() != NULL)
|
|
863 {
|
|
864 viewport3_->Refresh();
|
|
865 }
|
|
866
|
|
867 return true;
|
|
868 }
|
|
869 catch (Orthanc::OrthancException& e)
|
|
870 {
|
|
871 LOG(ERROR) << "Exception in the animation loop, stopping now: " << e.What();
|
|
872 return false;
|
|
873 }
|
|
874 }
|
|
875
|
|
876
|
823
|
877 static bool ctrlDown_ = false;
|
|
878
|
|
879
|
|
880 EM_BOOL OnMouseWheel(int eventType,
|
|
881 const EmscriptenWheelEvent *wheelEvent,
|
|
882 void *userData)
|
|
883 {
|
|
884 try
|
|
885 {
|
|
886 if (userData != NULL)
|
|
887 {
|
|
888 int delta = 0;
|
|
889
|
|
890 if (wheelEvent->deltaY < 0)
|
|
891 {
|
|
892 delta = -1;
|
|
893 }
|
|
894
|
|
895 if (wheelEvent->deltaY > 0)
|
|
896 {
|
|
897 delta = 1;
|
|
898 }
|
|
899
|
|
900 if (ctrlDown_)
|
|
901 {
|
|
902 delta *= 10;
|
|
903 }
|
|
904
|
|
905 reinterpret_cast<OrthancStone::VolumeSlicerViewport*>(userData)->Scroll(delta);
|
|
906 }
|
|
907 }
|
|
908 catch (Orthanc::OrthancException& e)
|
|
909 {
|
|
910 LOG(ERROR) << "Exception in the wheel event: " << e.What();
|
|
911 }
|
|
912
|
|
913 return true;
|
|
914 }
|
|
915
|
|
916
|
|
917 EM_BOOL OnKey(int eventType,
|
|
918 const EmscriptenKeyboardEvent *keyEvent,
|
|
919 void *userData)
|
|
920 {
|
|
921 ctrlDown_ = keyEvent->ctrlKey;
|
|
922 return false;
|
|
923 }
|
|
924
|
|
925
|
|
926
|
|
927
|
820
|
928 extern "C"
|
|
929 {
|
|
930 int main(int argc, char const *argv[])
|
|
931 {
|
|
932 OrthancStone::StoneInitialize();
|
|
933 Orthanc::Logging::EnableInfoLevel(true);
|
|
934 // Orthanc::Logging::EnableTraceLevel(true);
|
|
935 EM_ASM(window.dispatchEvent(new CustomEvent("WebAssemblyLoaded")););
|
|
936 }
|
|
937
|
|
938 EMSCRIPTEN_KEEPALIVE
|
|
939 void Initialize()
|
|
940 {
|
|
941 try
|
|
942 {
|
|
943 loader_.reset(new OrthancStone::OrthancSeriesVolumeProgressiveLoader(ct_, oracle_, oracle_));
|
|
944
|
|
945 viewport1_.reset(new OrthancStone::VolumeSlicerViewport(broker_, "mycanvas1", OrthancStone::VolumeProjection_Axial));
|
|
946 viewport1_->SetSlicer(0, loader_, *loader_, new OrthancStone::GrayscaleStyleConfigurator);
|
|
947 viewport1_->UpdateSize();
|
|
948
|
|
949 viewport2_.reset(new OrthancStone::VolumeSlicerViewport(broker_, "mycanvas2", OrthancStone::VolumeProjection_Coronal));
|
|
950 viewport2_->SetSlicer(0, loader_, *loader_, new OrthancStone::GrayscaleStyleConfigurator);
|
|
951 viewport2_->UpdateSize();
|
|
952
|
|
953 viewport3_.reset(new OrthancStone::VolumeSlicerViewport(broker_, "mycanvas3", OrthancStone::VolumeProjection_Sagittal));
|
|
954 viewport3_->SetSlicer(0, loader_, *loader_, new OrthancStone::GrayscaleStyleConfigurator);
|
|
955 viewport3_->UpdateSize();
|
|
956
|
|
957 emscripten_set_resize_callback("#window", NULL, false, OnWindowResize);
|
823
|
958
|
|
959 emscripten_set_wheel_callback("mycanvas1", viewport1_.get(), false, OnMouseWheel);
|
|
960 emscripten_set_wheel_callback("mycanvas2", viewport2_.get(), false, OnMouseWheel);
|
|
961 emscripten_set_wheel_callback("mycanvas3", viewport3_.get(), false, OnMouseWheel);
|
|
962
|
|
963 emscripten_set_keydown_callback("#window", NULL, false, OnKey);
|
|
964 emscripten_set_keyup_callback("#window", NULL, false, OnKey);
|
820
|
965
|
|
966 emscripten_request_animation_frame_loop(OnAnimationFrame, NULL);
|
|
967
|
|
968 oracle_.Start();
|
|
969 loader_->LoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa");
|
|
970 }
|
|
971 catch (Orthanc::OrthancException& e)
|
|
972 {
|
|
973 LOG(ERROR) << "Exception during Initialize(): " << e.What();
|
|
974 }
|
|
975 }
|
|
976 }
|