moved v3 docs
No files matched your search
@@ -1,95 +0,0 @@
|
|||||||
<div class='row'>
|
|
||||||
<div class='column' style='text-align: right; padding: 0 20px'>
|
|
||||||
<strong>Looking for a Live Demo?</strong>
|
|
||||||
<a href="http://viewer.ohif.org/">Preview The OHIF Viewer</a>
|
|
||||||
</div>
|
|
||||||
<div class='column' style='text-align: left; padding: 0 20px'>
|
|
||||||
<a href="https://www.netlify.com">
|
|
||||||
<img src="https://www.netlify.com/img/global/badges/netlify-color-bg.svg"/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
> ATTENTION! You are looking at the docs for the `OHIF-v3` Viewer (third time is always a charm). If you're looking for the our `OHIF-v2` (React stable version) or OHIF-v1 (deprecated `Meteor` version) select it's version from the dropdown box in the top left corner
|
|
||||||
> of this page.
|
|
||||||
|
|
||||||
|
|
||||||
# Introduction
|
|
||||||
|
|
||||||
The [Open Health Imaging Foundation][ohif-org] (OHIF) Viewer is an open source,
|
|
||||||
web-based, medical imaging viewer. It can be configured to connect to Image
|
|
||||||
Archives that support [DicomWeb][dicom-web], and offers support for mapping to
|
|
||||||
proprietary API formats. OHIF maintained extensions add support for viewing,
|
|
||||||
annotating, and reporting on DICOM images in 2D (slices) and 3D (volumes).
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
<center><i>The <strong>OHIF Viewer v3</strong>: A general purpose DICOM Viewer (<a href="http://viewer.ohif.org/">Live Demo</a>)</center>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## What's new in `OHIF-v3`
|
|
||||||
`OHIF-v3` is our second try for a React-based viewer, and is the third version of
|
|
||||||
our medical image web viewers from the start. The summary of changes include:
|
|
||||||
|
|
||||||
- Addition of workflow modes
|
|
||||||
|
|
||||||
- Often, medical imaging use cases involves lots of specific workflows that re-use
|
|
||||||
functionalities. We have added the capability of workflow modes, that enable people
|
|
||||||
to customize user interface and configure application for specific workflow.
|
|
||||||
- The idea is to re-use the functionalities that extensions provide and create a workflow.
|
|
||||||
Brain segmentation workflow is different from prostate segmentation in UI for sure; however, they share the segmentation tools that can be re-used.
|
|
||||||
- Our vision is that technical people focus of developing extensions which provides
|
|
||||||
core functionalities, and experts to build modes by picking the appropriate functionalities
|
|
||||||
from each extension.
|
|
||||||
|
|
||||||
|
|
||||||
- Redux store has been removed from the viewer, and a cleaner, more powerful
|
|
||||||
- tailwind CSS
|
|
||||||
- End-to-end test suite
|
|
||||||
|
|
||||||
|
|
||||||
Below, you can find the gap analysis between the `OHIF-v2` and `OHIF-v3`:
|
|
||||||
|
|
||||||
|
|
||||||
{% include "./_gap_analysis.md" %}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Where to next?
|
|
||||||
|
|
||||||
|
|
||||||
The Open Health Imaging Foundation intends to provide a simple general purpose
|
|
||||||
DICOM Viewer which can be easily extended for specific uses. If you find
|
|
||||||
yourself unable to extend the viewer for your purposes, please reach out via our
|
|
||||||
[GitHub issues][gh-issues]. We are actively seeking feedback on ways to improve
|
|
||||||
our integration and extension points.
|
|
||||||
|
|
||||||
|
|
||||||
Check out these helpful links:
|
|
||||||
|
|
||||||
- Ready to dive into some code? Check out our
|
|
||||||
[Getting Started Guide](./development/getting-started.md).
|
|
||||||
- We're an active, vibrant community.
|
|
||||||
[Learn how you can be more involved.](./development/contributing.md)
|
|
||||||
- Feeling lost? Read our [help page](./help.md).
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Links
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
|
||||||
[ohif-org]: http://www.ohif.org
|
|
||||||
[ohif-demo]: http://viewer.ohif.org/
|
|
||||||
[dicom-web]: https://en.wikipedia.org/wiki/DICOMweb
|
|
||||||
[gh-issues]: https://github.com/OHIF/Viewers/issues
|
|
||||||
<!-- prettier-ignore-end -->
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
|
||||||
<defs id="defs3051">
|
|
||||||
<style type="text/css" id="current-color-scheme">
|
|
||||||
.ColorScheme-Text {
|
|
||||||
color:#4d4d4d;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</defs>
|
|
||||||
<path
|
|
||||||
style="fill:currentColor;fill-opacity:1;stroke:none"
|
|
||||||
d="M 7.5 2 A 4.5 4.5 0 0 0 3 6.5 A 4.5 4.5 0 0 0 3.0527344 7.15625 A 3 3 0 0 0 1 10 A 3 3 0 0 0 4 13 L 11.5 13 A 3.5 3.5 0 0 0 15 9.5 A 3.5 3.5 0 0 0 11.970703 6.0351562 A 4.5 4.5 0 0 0 7.5 2 z M 7.5 3 A 3.5 3.5 0 0 1 11 6.5 A 3.5 3.5 0 0 1 10.949219 7.0644531 A 2.5 2.5 0 0 1 11.5 7 A 2.5 2.5 0 0 1 14 9.5 A 2.5 2.5 0 0 1 11.5 12 L 4 12 A 2 2 0 0 1 2 10 A 2 2 0 0 1 4 8 A 2 2 0 0 1 4.3574219 8.0332031 A 3.5 3.5 0 0 1 4 6.5 A 3.5 3.5 0 0 1 7.5 3 z "
|
|
||||||
id="path4139"
|
|
||||||
class="ColorScheme-Text"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 818 B |
@@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<svg version="1.1" id="npm" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
||||||
viewBox="0 0 780 250" style="enable-background:new 0 0 780 250;" xml:space="preserve">
|
|
||||||
<style type="text/css">
|
|
||||||
.st0{fill:#C12127;}
|
|
||||||
</style>
|
|
||||||
<path class="st0" d="M240,250h100v-50h100V0H240V250z M340,50h50v100h-50V50z M480,0v200h100V50h50v150h50V50h50v150h50V0H480z
|
|
||||||
M0,200h100V50h50v150h50V0H0V200z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 473 B |
|
Before Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 422 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 884 KiB |
@@ -1,187 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg width="203px" height="39px" viewBox="0 0 203 39" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<!-- Generator: Sketch 54.1 (76490) - https://sketchapp.com -->
|
|
||||||
<title>Custom Preset 2 Copy</title>
|
|
||||||
<desc>Created with Sketch.</desc>
|
|
||||||
<defs>
|
|
||||||
<filter x="-22.4%" y="-22.4%" width="144.8%" height="144.8%" filterUnits="objectBoundingBox" id="filter-1">
|
|
||||||
<feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
|
|
||||||
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
|
|
||||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.4707441 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
|
|
||||||
<feMerge>
|
|
||||||
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
|
|
||||||
<feMergeNode in="SourceGraphic"></feMergeNode>
|
|
||||||
</feMerge>
|
|
||||||
</filter>
|
|
||||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176" id="path-2"></path>
|
|
||||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176 L0.103703704,13.9901176 Z" id="path-4"></path>
|
|
||||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176" id="path-6"></path>
|
|
||||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176 L0.103703704,13.9901176 Z" id="path-8"></path>
|
|
||||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176" id="path-10"></path>
|
|
||||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176 L0.103703704,13.9901176 Z" id="path-12"></path>
|
|
||||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176" id="path-14"></path>
|
|
||||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176 L0.103703704,13.9901176 Z" id="path-16"></path>
|
|
||||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176" id="path-18"></path>
|
|
||||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176 L0.103703704,13.9901176 Z" id="path-20"></path>
|
|
||||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176" id="path-22"></path>
|
|
||||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176 L0.103703704,13.9901176 Z" id="path-24"></path>
|
|
||||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176" id="path-26"></path>
|
|
||||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176 L0.103703704,13.9901176 Z" id="path-28"></path>
|
|
||||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176" id="path-30"></path>
|
|
||||||
<path d="M0.103703704,13.9901176 C0.103703704,21.6217647 6.3365037,27.8085294 14.0246815,27.8085294 L14.0246815,27.8085294 C21.7130667,27.8085294 27.9456593,21.6217647 27.9456593,13.9901176 L27.9456593,13.9901176 C27.9456593,6.35805882 21.7130667,0.171294118 14.0246815,0.171294118 L14.0246815,0.171294118 C6.3365037,0.171294118 0.103703704,6.35805882 0.103703704,13.9901176 L0.103703704,13.9901176 Z" id="path-32"></path>
|
|
||||||
<polyline id="path-34" points="1.7512 17.989 11.9476 17.989 12.85 0.35 0.05 0.1"></polyline>
|
|
||||||
<polyline id="path-36" points="1.7512 17.989 11.9476 17.989 12.85 0.35 0.05 0.1"></polyline>
|
|
||||||
<polygon id="path-38" points="1.7512 17.989 11.9476 17.989 12.85 0.35 0.05 0.1"></polygon>
|
|
||||||
</defs>
|
|
||||||
<g id="Custom-Preset-2-Copy" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
|
||||||
<rect fill="#FFFFFF" x="0" y="0" width="203" height="39"></rect>
|
|
||||||
<g id="Group-65" filter="url(#filter-1)" transform="translate(6.000000, 5.000000)">
|
|
||||||
<g id="Group-49" transform="translate(1.000000, 1.000000)">
|
|
||||||
<g id="Group-3">
|
|
||||||
<mask id="mask-3" fill="white">
|
|
||||||
<use xlink:href="#path-2"></use>
|
|
||||||
</mask>
|
|
||||||
<g id="Clip-2"></g>
|
|
||||||
<path d="M1.85138074,6.7158 C1.69250667,7.85927059 2.08948444,8.45077059 1.85138074,9.00253529 C1.61306963,9.55450588 1.3349363,10.5402706 1.57324741,11.2104176 C1.81155852,11.8805647 2.16892148,13.3392412 2.48666963,13.3789765 C2.80441778,13.4180941 2.24856593,11.8019176 2.56610667,12.3143588 C2.88406222,12.8270059 2.88406222,13.5893882 3.44012148,14.1150059 C3.99597333,14.6406235 4.75072889,14.5617706 5.34661037,15.0740059 C5.94228444,15.5866529 6.53795852,16.3358588 7.09401778,16.2570059 C7.09401778,16.2570059 7.05419556,16.6510647 6.77626963,17.3214176 C6.4983437,17.9915647 6.45852148,18.7798882 6.81609185,19.2532118 C7.17345481,19.7263294 7.53081778,20.4362118 7.84877333,20.7909471 C8.16652148,21.1456824 8.40483259,23.0381529 8.32518815,23.9841824 C8.24595852,24.9304176 8.68275852,27.0594471 9.67561778,27.5327706 C10.668477,28.0058882 9.67561778,27.2960059 9.47712889,26.7047118 C9.27843259,26.1134176 9.63040296,24.7333882 10.1101363,24.5361529 C10.58904,24.3389176 11.0260474,23.5505941 11.3834104,22.9984176 C11.7409807,22.4466529 12.654403,22.2098882 12.9325363,21.5399471 C13.2106696,20.8698 13.0914104,20.0810647 13.2502844,19.7263294 C13.4091585,19.3713882 13.8461659,18.5828588 12.8927141,18.1097412 C11.939677,17.6366235 11.5424919,17.8733882 11.5026696,17.4395941 C11.4628474,17.0058 11.2245363,16.5328882 10.5496326,16.3358588 C9.87431407,16.1384176 9.27843259,15.7560941 8.52367704,15.6714765 C7.76912889,15.5866529 7.61025481,16.1781529 7.21306963,15.8625353 C6.81609185,15.5473294 6.4983437,15.2317118 6.33926222,14.8376529 C6.18059556,14.4433882 5.70376593,14.6799471 5.74379556,14.4433882 C5.78320296,14.2066235 6.41869926,13.5366824 5.70376593,13.7732412 C4.98904,14.0095941 4.3535437,13.9307412 4.39315852,13.3001235 C4.43318815,12.6693 5.30678815,12.4325353 5.66435852,12.3536824 C6.02151407,12.2748294 6.29964741,12.7084176 6.73665481,12.9847118 C7.17345481,13.2605941 6.77626963,12.5904471 7.01458074,12.1171235 C7.25289185,11.6440059 8.16652148,11.6835353 8.28557333,11.0133882 C8.40483259,10.3432412 8.24595852,9.79106471 8.96089185,9.71221176 C9.67561778,9.63335882 10.4301659,9.75153529 10.5098104,9.39659412 C10.58904,9.04206471 9.67561778,9.51518235 9.63600296,9.08138824 C9.59618074,8.6478 10.4301659,8.05650588 10.668477,8.49009412 C10.9067881,8.92368235 11.5026696,8.96321176 11.5821067,8.8053 C11.6615437,8.6478 11.5821067,8.37191765 11.3039733,7.70156471 C11.0260474,7.03121176 10.7479141,7.22844706 10.5098104,6.47944706 C10.2714993,5.73024118 9.87431407,6.51877059 9.47712889,6.12450588 C9.0799437,5.73024118 8.60332148,4.90218235 8.52367704,5.61206471 C8.44444741,6.32153529 8.44444741,6.7158 8.08708444,7.26777059 C7.72951407,7.81994706 7.57084741,7.38615294 7.25289185,7.03121176 C6.9351437,6.67647647 6.22021037,6.32153529 6.45852148,6.00632941 C6.69683259,5.69071176 6.9351437,4.86285882 7.29250667,5.09962353 C7.65007704,5.33618235 8.2061363,5.33618235 8.12669926,4.94171176 C8.04726222,4.54765294 8.12669926,4.38974118 8.28557333,4.19271176 C8.44444741,3.99547647 9.00050667,3.64053529 9.00050667,4.35041765 C9.00050667,5.06009412 8.92086222,5.25732941 9.47712889,5.33618235 C10.0331881,5.41503529 10.668477,5.45435882 10.6290696,5.09962353 C10.58904,4.74468235 10.7879437,4.82353529 10.6290696,4.23203529 C10.4699881,3.64053529 9.99336593,3.60141765 9.47712889,3.32512353 C8.96089185,3.04924118 8.68275852,2.61565294 8.24595852,2.77335882 C7.80915852,2.93106471 7.76912889,3.08856471 7.72951407,3.52235882 C7.68969185,3.95615294 7.45138074,4.11385882 7.29250667,3.91662353 C7.13384,3.71959412 7.33232889,3.08856471 6.73665481,3.48303529 C6.14077333,3.87709412 5.90266963,4.54765294 5.58492148,4.07432941 C5.26696593,3.60141765 6.0613363,2.77335882 5.22735111,3.1283 C4.39315852,3.48303529 3.79727704,3.87709412 4.23428444,4.03500588 C4.67129185,4.19271176 5.10829926,4.4688 4.67129185,4.62650588 C4.23428444,4.78400588 3.36047704,4.50812353 3.04272889,4.82353529 C2.72498074,5.13874118 1.85138074,6.7158 1.85138074,6.7158" id="Fill-1" fill="#2196F3" mask="url(#mask-3)"></path>
|
|
||||||
</g>
|
|
||||||
<g id="Group-6">
|
|
||||||
<mask id="mask-5" fill="white">
|
|
||||||
<use xlink:href="#path-4"></use>
|
|
||||||
</mask>
|
|
||||||
<g id="Clip-5"></g>
|
|
||||||
<path d="M1.85138074,6.7158 C1.69250667,7.85927059 2.08948444,8.45077059 1.85138074,9.00253529 C1.61306963,9.55450588 1.3349363,10.5402706 1.57324741,11.2104176 C1.81155852,11.8805647 2.16892148,13.3392412 2.48666963,13.3789765 C2.80441778,13.4180941 2.24856593,11.8019176 2.56610667,12.3143588 C2.88406222,12.8270059 2.88406222,13.5893882 3.44012148,14.1150059 C3.99597333,14.6406235 4.75072889,14.5617706 5.34661037,15.0740059 C5.94228444,15.5866529 6.53795852,16.3358588 7.09401778,16.2570059 C7.09401778,16.2570059 7.05419556,16.6510647 6.77626963,17.3214176 C6.4983437,17.9915647 6.45852148,18.7798882 6.81609185,19.2532118 C7.17345481,19.7263294 7.53081778,20.4362118 7.84877333,20.7909471 C8.16652148,21.1456824 8.40483259,23.0381529 8.32518815,23.9841824 C8.24595852,24.9304176 8.68275852,27.0594471 9.67561778,27.5327706 C10.668477,28.0058882 9.67561778,27.2960059 9.47712889,26.7047118 C9.27843259,26.1134176 9.63040296,24.7333882 10.1101363,24.5361529 C10.58904,24.3389176 11.0260474,23.5505941 11.3834104,22.9984176 C11.7409807,22.4466529 12.654403,22.2098882 12.9325363,21.5399471 C13.2106696,20.8698 13.0914104,20.0810647 13.2502844,19.7263294 C13.4091585,19.3713882 13.8461659,18.5828588 12.8927141,18.1097412 C11.939677,17.6366235 11.5424919,17.8733882 11.5026696,17.4395941 C11.4628474,17.0058 11.2245363,16.5328882 10.5496326,16.3358588 C9.87431407,16.1384176 9.27843259,15.7560941 8.52367704,15.6714765 C7.76912889,15.5866529 7.61025481,16.1781529 7.21306963,15.8625353 C6.81609185,15.5473294 6.4983437,15.2317118 6.33926222,14.8376529 C6.18059556,14.4433882 5.70376593,14.6799471 5.74379556,14.4433882 C5.78320296,14.2066235 6.41869926,13.5366824 5.70376593,13.7732412 C4.98904,14.0095941 4.3535437,13.9307412 4.39315852,13.3001235 C4.43318815,12.6693 5.30678815,12.4325353 5.66435852,12.3536824 C6.02151407,12.2748294 6.29964741,12.7084176 6.73665481,12.9847118 C7.17345481,13.2605941 6.77626963,12.5904471 7.01458074,12.1171235 C7.25289185,11.6440059 8.16652148,11.6835353 8.28557333,11.0133882 C8.40483259,10.3432412 8.24595852,9.79106471 8.96089185,9.71221176 C9.67561778,9.63335882 10.4301659,9.75153529 10.5098104,9.39659412 C10.58904,9.04206471 9.67561778,9.51518235 9.63600296,9.08138824 C9.59618074,8.6478 10.4301659,8.05650588 10.668477,8.49009412 C10.9067881,8.92368235 11.5026696,8.96321176 11.5821067,8.8053 C11.6615437,8.6478 11.5821067,8.37191765 11.3039733,7.70156471 C11.0260474,7.03121176 10.7479141,7.22844706 10.5098104,6.47944706 C10.2714993,5.73024118 9.87431407,6.51877059 9.47712889,6.12450588 C9.0799437,5.73024118 8.60332148,4.90218235 8.52367704,5.61206471 C8.44444741,6.32153529 8.44444741,6.7158 8.08708444,7.26777059 C7.72951407,7.81994706 7.57084741,7.38615294 7.25289185,7.03121176 C6.9351437,6.67647647 6.22021037,6.32153529 6.45852148,6.00632941 C6.69683259,5.69071176 6.9351437,4.86285882 7.29250667,5.09962353 C7.65007704,5.33618235 8.2061363,5.33618235 8.12669926,4.94171176 C8.04726222,4.54765294 8.12669926,4.38974118 8.28557333,4.19271176 C8.44444741,3.99547647 9.00050667,3.64053529 9.00050667,4.35041765 C9.00050667,5.06009412 8.92086222,5.25732941 9.47712889,5.33618235 C10.0331881,5.41503529 10.668477,5.45435882 10.6290696,5.09962353 C10.58904,4.74468235 10.7879437,4.82353529 10.6290696,4.23203529 C10.4699881,3.64053529 9.99336593,3.60141765 9.47712889,3.32512353 C8.96089185,3.04924118 8.68275852,2.61565294 8.24595852,2.77335882 C7.80915852,2.93106471 7.76912889,3.08856471 7.72951407,3.52235882 C7.68969185,3.95615294 7.45138074,4.11385882 7.29250667,3.91662353 C7.13384,3.71959412 7.33232889,3.08856471 6.73665481,3.48303529 C6.14077333,3.87709412 5.90266963,4.54765294 5.58492148,4.07432941 C5.26696593,3.60141765 6.0613363,2.77335882 5.22735111,3.1283 C4.39315852,3.48303529 3.79727704,3.87709412 4.23428444,4.03500588 C4.67129185,4.19271176 5.10829926,4.4688 4.67129185,4.62650588 C4.23428444,4.78400588 3.36047704,4.50812353 3.04272889,4.82353529 C2.72498074,5.13874118 1.85138074,6.7158 1.85138074,6.7158 Z" id="Stroke-4" stroke="#2196F3" mask="url(#mask-5)"></path>
|
|
||||||
</g>
|
|
||||||
<g id="Group-9">
|
|
||||||
<mask id="mask-7" fill="white">
|
|
||||||
<use xlink:href="#path-6"></use>
|
|
||||||
</mask>
|
|
||||||
<g id="Clip-8"></g>
|
|
||||||
<path d="M6.5378963,2.45802941 C6.53582222,2.89882353 6.65715556,3.24635294 7.05434074,2.85229412 C7.45152593,2.45802941 7.68962963,1.98491176 7.96797037,2.18194118 C8.2458963,2.37917647 8.72251852,2.32667647 8.9208,2.3135 C9.1197037,2.30032353 9.15931852,1.70882353 9.71537778,1.78767647 C10.271437,1.86652941 10.0729481,1.6695 10.6686222,1.35388235 C11.2645037,1.03847059 12.5354963,0.486705882 11.2246815,0.644411765 C9.91386667,0.802117647 9.83442963,0.447176471 8.9208,1.03847059 C8.00758519,1.62997059 7.8090963,1.62997059 7.57078519,1.78767647 C7.33247407,1.94538235 7.05434074,1.31455882 6.73659259,1.78767647 C6.41863704,2.26079412 6.54017778,2.00879412 6.5378963,2.45802941" id="Fill-7" fill="#2196F3" mask="url(#mask-7)"></path>
|
|
||||||
</g>
|
|
||||||
<g id="Group-12">
|
|
||||||
<mask id="mask-9" fill="white">
|
|
||||||
<use xlink:href="#path-8"></use>
|
|
||||||
</mask>
|
|
||||||
<g id="Clip-11"></g>
|
|
||||||
<path d="M6.5378963,2.45802941 C6.53582222,2.89882353 6.65715556,3.24635294 7.05434074,2.85229412 C7.45152593,2.45802941 7.68962963,1.98491176 7.96797037,2.18194118 C8.2458963,2.37917647 8.72251852,2.32667647 8.9208,2.3135 C9.1197037,2.30032353 9.15931852,1.70882353 9.71537778,1.78767647 C10.271437,1.86652941 10.0729481,1.6695 10.6686222,1.35388235 C11.2645037,1.03847059 12.5354963,0.486705882 11.2246815,0.644411765 C9.91386667,0.802117647 9.83442963,0.447176471 8.9208,1.03847059 C8.00758519,1.62997059 7.8090963,1.62997059 7.57078519,1.78767647 C7.33247407,1.94538235 7.05434074,1.31455882 6.73659259,1.78767647 C6.41863704,2.26079412 6.54017778,2.00879412 6.5378963,2.45802941 Z" id="Stroke-10" stroke="#2196F3" mask="url(#mask-9)"></path>
|
|
||||||
</g>
|
|
||||||
<g id="Group-15">
|
|
||||||
<mask id="mask-11" fill="white">
|
|
||||||
<use xlink:href="#path-10"></use>
|
|
||||||
</mask>
|
|
||||||
<g id="Clip-14"></g>
|
|
||||||
<path d="M11.3040148,1.23570588 C10.8346519,1.63841176 10.1126667,2.22147059 10.6685185,2.22147059 C11.2245778,2.22147059 11.4946222,1.93241176 11.7966074,2.3135 C12.0985926,2.69438235 11.9791259,3.28588235 11.9791259,3.56176471 C11.9791259,3.83785294 11.8202519,4.46888235 12.2176444,5.25720588 C12.6148296,6.04552941 12.6544444,6.55817647 13.051837,6.04552941 C13.4488148,5.53329412 14.4020593,4.54752941 14.8786815,4.46888235 C15.3553037,4.38982353 15.8323407,4.19258824 16.0704444,3.79832353 C16.3085481,3.40405882 16.0704444,3.02152941 16.4674222,2.73988235 C16.8644,2.45802941 16.784963,2.10308824 16.9044296,1.6695 C17.0232741,1.23570588 18.0959852,1.23570588 17.6191556,0.999352941 C17.1427407,0.762588235 16.2689333,0.604882353 15.2360444,0.723264706 C14.203363,0.841441176 12.1778222,0.486705882 11.3040148,1.23570588" id="Fill-13" fill="#2196F3" mask="url(#mask-11)"></path>
|
|
||||||
</g>
|
|
||||||
<g id="Group-18">
|
|
||||||
<mask id="mask-13" fill="white">
|
|
||||||
<use xlink:href="#path-12"></use>
|
|
||||||
</mask>
|
|
||||||
<g id="Clip-17"></g>
|
|
||||||
<path d="M11.3040148,1.23570588 C10.8346519,1.63841176 10.1126667,2.22147059 10.6685185,2.22147059 C11.2245778,2.22147059 11.4946222,1.93241176 11.7966074,2.3135 C12.0985926,2.69438235 11.9791259,3.28588235 11.9791259,3.56176471 C11.9791259,3.83785294 11.8202519,4.46888235 12.2176444,5.25720588 C12.6148296,6.04552941 12.6544444,6.55817647 13.051837,6.04552941 C13.4488148,5.53329412 14.4020593,4.54752941 14.8786815,4.46888235 C15.3553037,4.38982353 15.8323407,4.19258824 16.0704444,3.79832353 C16.3085481,3.40405882 16.0704444,3.02152941 16.4674222,2.73988235 C16.8644,2.45802941 16.784963,2.10308824 16.9044296,1.6695 C17.0232741,1.23570588 18.0959852,1.23570588 17.6191556,0.999352941 C17.1427407,0.762588235 16.2689333,0.604882353 15.2360444,0.723264706 C14.203363,0.841441176 12.1778222,0.486705882 11.3040148,1.23570588 Z" id="Stroke-16" stroke="#2196F3" mask="url(#mask-13)"></path>
|
|
||||||
</g>
|
|
||||||
<g id="Group-21">
|
|
||||||
<mask id="mask-15" fill="white">
|
|
||||||
<use xlink:href="#path-14"></use>
|
|
||||||
</mask>
|
|
||||||
<g id="Clip-20"></g>
|
|
||||||
<path d="M6.93518519,13.4181353 C6.68256296,13.8270176 6.81592593,13.8519294 7.09405926,13.9307824 C7.37198519,14.0096353 6.9748,14.4825471 7.29254815,14.5220765 C7.6102963,14.5618118 8.32522963,14.7588412 8.52371852,14.5220765 C8.72262222,14.2857235 8.20617778,14.3779588 7.84860741,14.1150471 C7.49145185,13.8519294 7.11459259,13.1278412 6.93518519,13.4181353" id="Fill-19" fill="#2196F3" mask="url(#mask-15)"></path>
|
|
||||||
</g>
|
|
||||||
<g id="Group-24">
|
|
||||||
<mask id="mask-17" fill="white">
|
|
||||||
<use xlink:href="#path-16"></use>
|
|
||||||
</mask>
|
|
||||||
<g id="Clip-23"></g>
|
|
||||||
<path d="M6.93518519,13.4181353 C6.68256296,13.8270176 6.81592593,13.8519294 7.09405926,13.9307824 C7.37198519,14.0096353 6.9748,14.4825471 7.29254815,14.5220765 C7.6102963,14.5618118 8.32522963,14.7588412 8.52371852,14.5220765 C8.72262222,14.2857235 8.20617778,14.3779588 7.84860741,14.1150471 C7.49145185,13.8519294 7.11459259,13.1278412 6.93518519,13.4181353 Z" id="Stroke-22" stroke="#2196F3" mask="url(#mask-17)"></path>
|
|
||||||
</g>
|
|
||||||
<g id="Group-27">
|
|
||||||
<mask id="mask-19" fill="white">
|
|
||||||
<use xlink:href="#path-18"></use>
|
|
||||||
</mask>
|
|
||||||
<g id="Clip-26"></g>
|
|
||||||
<path d="M26.1584504,19.8445265 C25.7305689,20.0572029 25.9203467,20.3967029 25.5229541,20.6332618 C25.1257689,20.8696147 25.284643,20.9484676 25.1655911,21.5006441 C25.0463319,22.0524088 25.2052059,22.3678206 25.4833393,21.9342324 C25.7614726,21.5006441 26.6350726,19.6081735 26.1584504,19.8445265" id="Fill-25" fill="#2196F3" mask="url(#mask-19)"></path>
|
|
||||||
</g>
|
|
||||||
<g id="Group-30">
|
|
||||||
<mask id="mask-21" fill="white">
|
|
||||||
<use xlink:href="#path-20"></use>
|
|
||||||
</mask>
|
|
||||||
<g id="Clip-29"></g>
|
|
||||||
<path d="M26.1584504,19.8445265 C25.7305689,20.0572029 25.9203467,20.3967029 25.5229541,20.6332618 C25.1257689,20.8696147 25.284643,20.9484676 25.1655911,21.5006441 C25.0463319,22.0524088 25.2052059,22.3678206 25.4833393,21.9342324 C25.7614726,21.5006441 26.6350726,19.6081735 26.1584504,19.8445265 Z" id="Stroke-28" stroke="#2196F3" mask="url(#mask-21)"></path>
|
|
||||||
</g>
|
|
||||||
<g id="Group-33">
|
|
||||||
<mask id="mask-23" fill="white">
|
|
||||||
<use xlink:href="#path-22"></use>
|
|
||||||
</mask>
|
|
||||||
<g id="Clip-32"></g>
|
|
||||||
<path d="M26.5159378,11.4075706 C26.3966785,11.5259529 26.1981896,10.8556 26.0791378,10.5401882 C25.9598785,10.2247765 25.9627822,9.67651176 26.29816,9.37180588 C26.7158785,8.99174706 26.5159378,9.86983529 26.5159378,10.3431588 C26.5159378,10.8160706 26.6349896,11.2891882 26.5159378,11.4075706 M25.1254785,10.1854529 C24.8473452,10.0277471 24.331523,10.1854529 23.6960267,10.3824824 C23.060323,10.5797176 23.5688859,9.94374706 23.8549007,9.5151 C24.1106341,9.13195294 23.9739526,9.63327647 24.5302193,9.67280588 C25.0860711,9.71233529 25.4040267,10.3431588 25.1254785,10.1854529 M24.2124711,11.5259529 C24.1726489,11.8411588 24.331523,12.3538059 23.4577156,12.1567765 C22.5839081,11.9595412 22.5839081,11.4864235 22.2265452,11.8411588 C21.8691822,12.1961 21.4321748,12.5113059 20.9157304,11.9595412 C20.3994933,11.4075706 20.4789304,11.2498647 20.2010044,11.2103353 C20.0029304,11.1825412 19.3427526,11.0361588 18.7922933,11.1951 C18.8350193,11.1697765 18.8806489,11.1364235 18.9298044,11.0921588 C19.3269896,10.7374235 19.2475526,10.6190412 19.6843526,10.2247765 C20.1215674,9.83030588 20.4391081,9.47577647 20.7568563,9.83030588 C21.0746044,10.1854529 21.749923,10.2643059 21.5116119,10.6585706 C21.2733007,11.0526294 21.4321748,11.2891882 21.7103081,11.0526294 C21.9880267,10.8160706 22.3854193,10.6585706 22.1074933,10.3431588 C21.8291526,10.0277471 21.5510193,9.19968824 21.9486193,9.75165882 C22.3458044,10.3036294 22.4648563,10.5797176 22.70296,10.8949235 C22.9414785,11.2103353 23.1797896,11.4075706 23.2592267,11.2103353 C23.3386637,11.0133059 23.2194044,10.5797176 23.3386637,10.5008647 C23.4577156,10.4220118 23.3386637,11.1710118 23.6960267,11.2103353 C24.0533896,11.2498647 24.2520859,11.2103353 24.2124711,11.5259529 M28.4224267,13.2213941 C28.3033748,12.5508353 27.4295674,6.47936471 27.4295674,6.47936471 C27.4295674,6.47936471 25.0860711,4.98115882 24.6890933,5.29636471 C24.2917007,5.61198235 23.6165896,5.56442353 23.6960267,5.25312941 C23.7754637,4.94183529 24.2917007,5.02089412 23.4176859,4.42918824 C22.5442933,3.83768824 22.3854193,3.32524706 21.7103081,4.07424706 C21.0347822,4.82345294 20.8761156,5.45427647 20.4789304,5.65151176 C20.0817452,5.84854118 19.763997,6.47936471 20.2010044,6.63707059 C20.6378044,6.79457059 20.6776267,6.63707059 21.0347822,6.95227647 C21.39256,7.26789412 21.8691822,7.18883529 21.9486193,6.99201176 C22.0280563,6.79457059 22.1074933,6.16395294 22.1471081,5.92739412 C22.1869304,5.69083529 22.5905452,4.90230588 22.5474044,5.53312941 C22.5044711,6.16395294 23.1001452,6.16395294 22.70296,6.59754118 C22.3059822,7.03112941 22.5442933,7.38627647 21.749923,7.46512941 C20.9553452,7.54377647 20.9157304,7.07086471 20.4789304,7.30742353 C20.041923,7.54377647 20.0817452,7.70168824 19.9228711,7.93804118 C19.763997,8.1746 18.9298044,8.64771765 18.9298044,8.64771765 C18.9298044,8.64771765 18.2546933,8.41115882 18.5722341,8.84495294 C18.8901896,9.27854118 19.1681156,9.79098235 18.8901896,9.83030588 C18.6120563,9.86983529 17.8573007,9.43624706 17.8573007,9.90936471 C17.8573007,10.3824824 17.7384563,10.8949235 18.0562044,11.0133059 C18.2804119,11.0966882 18.4260119,11.2776588 18.63176,11.2523353 C18.4722637,11.3206882 18.3272859,11.4197176 18.2150785,11.5652765 C17.9058341,11.9661294 18.0958193,12.6692176 17.460323,12.9057765 C16.8250341,13.1425412 16.8250341,13.4182176 16.7055674,13.9308647 C16.586723,14.4433059 16.7055674,15.7442765 16.8644415,15.8626588 C17.0231081,15.9808353 17.3014489,16.9666 17.9767674,16.8875412 C18.6520859,16.8086882 19.3668119,16.7784235 19.605123,16.8922765 C19.8434341,17.0057176 20.2404119,16.6511882 20.3596711,17.5578941 C20.4789304,18.4646 20.7568563,18.5829824 20.9157304,19.0561 C21.0746044,19.5290118 20.8362933,20.5149824 20.9157304,21.1062765 C20.9951674,21.6975706 21.5510193,22.7228647 21.5510193,23.2353059 C21.5510193,23.7477471 22.1471081,23.9843059 22.4648563,23.7872765 C22.7826044,23.5900412 23.4577156,23.1957765 23.7356415,22.5649529 C24.0139822,21.9343353 24.0139822,21.6189235 24.4505748,20.7908647 C24.8873748,19.9630118 24.7685304,19.3715118 24.9668119,18.7406882 C25.1655081,18.1098647 25.6433748,17.6760706 26.29816,16.9666 C26.9527378,16.2571294 26.8349304,15.8626588 26.29816,15.9019824 C25.7613896,15.9413059 25.4034044,15.4290706 25.1254785,14.7982471 C24.8479674,14.1672176 24.5302193,12.5903647 24.7685304,13.1028059 C25.0068415,13.6156588 25.4832563,15.0348059 25.9598785,15.2713647 C26.4365007,15.5079235 27.3105156,14.8692765 27.5090044,14.6366294 C27.7074933,14.4037765 27.6280563,13.7338353 27.1116119,13.4577471 C26.5953748,13.1816588 26.913123,12.9453059 27.4295674,13.2605118 C27.9458044,13.5759235 28.5416859,13.8913353 28.4224267,13.2213941" id="Fill-31" fill="#2196F3" mask="url(#mask-23)"></path>
|
|
||||||
</g>
|
|
||||||
<g id="Group-36">
|
|
||||||
<mask id="mask-25" fill="white">
|
|
||||||
<use xlink:href="#path-24"></use>
|
|
||||||
</mask>
|
|
||||||
<g id="Clip-35"></g>
|
|
||||||
<path d="M26.5159378,11.4075706 C26.3966785,11.5259529 26.1981896,10.8556 26.0791378,10.5401882 C25.9598785,10.2247765 25.9627822,9.67651176 26.29816,9.37180588 C26.7158785,8.99174706 26.5159378,9.86983529 26.5159378,10.3431588 C26.5159378,10.8160706 26.6349896,11.2891882 26.5159378,11.4075706 Z M25.1254785,10.1854529 C24.8473452,10.0277471 24.331523,10.1854529 23.6960267,10.3824824 C23.060323,10.5797176 23.5688859,9.94374706 23.8549007,9.5151 C24.1106341,9.13195294 23.9739526,9.63327647 24.5302193,9.67280588 C25.0860711,9.71233529 25.4040267,10.3431588 25.1254785,10.1854529 Z M24.2124711,11.5259529 C24.1726489,11.8411588 24.331523,12.3538059 23.4577156,12.1567765 C22.5839081,11.9595412 22.5839081,11.4864235 22.2265452,11.8411588 C21.8691822,12.1961 21.4321748,12.5113059 20.9157304,11.9595412 C20.3994933,11.4075706 20.4789304,11.2498647 20.2010044,11.2103353 C20.0029304,11.1825412 19.3427526,11.0361588 18.7922933,11.1951 C18.8350193,11.1697765 18.8806489,11.1364235 18.9298044,11.0921588 C19.3269896,10.7374235 19.2475526,10.6190412 19.6843526,10.2247765 C20.1215674,9.83030588 20.4391081,9.47577647 20.7568563,9.83030588 C21.0746044,10.1854529 21.749923,10.2643059 21.5116119,10.6585706 C21.2733007,11.0526294 21.4321748,11.2891882 21.7103081,11.0526294 C21.9880267,10.8160706 22.3854193,10.6585706 22.1074933,10.3431588 C21.8291526,10.0277471 21.5510193,9.19968824 21.9486193,9.75165882 C22.3458044,10.3036294 22.4648563,10.5797176 22.70296,10.8949235 C22.9414785,11.2103353 23.1797896,11.4075706 23.2592267,11.2103353 C23.3386637,11.0133059 23.2194044,10.5797176 23.3386637,10.5008647 C23.4577156,10.4220118 23.3386637,11.1710118 23.6960267,11.2103353 C24.0533896,11.2498647 24.2520859,11.2103353 24.2124711,11.5259529 Z M28.4224267,13.2213941 C28.3033748,12.5508353 27.4295674,6.47936471 27.4295674,6.47936471 C27.4295674,6.47936471 25.0860711,4.98115882 24.6890933,5.29636471 C24.2917007,5.61198235 23.6165896,5.56442353 23.6960267,5.25312941 C23.7754637,4.94183529 24.2917007,5.02089412 23.4176859,4.42918824 C22.5442933,3.83768824 22.3854193,3.32524706 21.7103081,4.07424706 C21.0347822,4.82345294 20.8761156,5.45427647 20.4789304,5.65151176 C20.0817452,5.84854118 19.763997,6.47936471 20.2010044,6.63707059 C20.6378044,6.79457059 20.6776267,6.63707059 21.0347822,6.95227647 C21.39256,7.26789412 21.8691822,7.18883529 21.9486193,6.99201176 C22.0280563,6.79457059 22.1074933,6.16395294 22.1471081,5.92739412 C22.1869304,5.69083529 22.5905452,4.90230588 22.5474044,5.53312941 C22.5044711,6.16395294 23.1001452,6.16395294 22.70296,6.59754118 C22.3059822,7.03112941 22.5442933,7.38627647 21.749923,7.46512941 C20.9553452,7.54377647 20.9157304,7.07086471 20.4789304,7.30742353 C20.041923,7.54377647 20.0817452,7.70168824 19.9228711,7.93804118 C19.763997,8.1746 18.9298044,8.64771765 18.9298044,8.64771765 C18.9298044,8.64771765 18.2546933,8.41115882 18.5722341,8.84495294 C18.8901896,9.27854118 19.1681156,9.79098235 18.8901896,9.83030588 C18.6120563,9.86983529 17.8573007,9.43624706 17.8573007,9.90936471 C17.8573007,10.3824824 17.7384563,10.8949235 18.0562044,11.0133059 C18.2804119,11.0966882 18.4260119,11.2776588 18.63176,11.2523353 C18.4722637,11.3206882 18.3272859,11.4197176 18.2150785,11.5652765 C17.9058341,11.9661294 18.0958193,12.6692176 17.460323,12.9057765 C16.8250341,13.1425412 16.8250341,13.4182176 16.7055674,13.9308647 C16.586723,14.4433059 16.7055674,15.7442765 16.8644415,15.8626588 C17.0231081,15.9808353 17.3014489,16.9666 17.9767674,16.8875412 C18.6520859,16.8086882 19.3668119,16.7784235 19.605123,16.8922765 C19.8434341,17.0057176 20.2404119,16.6511882 20.3596711,17.5578941 C20.4789304,18.4646 20.7568563,18.5829824 20.9157304,19.0561 C21.0746044,19.5290118 20.8362933,20.5149824 20.9157304,21.1062765 C20.9951674,21.6975706 21.5510193,22.7228647 21.5510193,23.2353059 C21.5510193,23.7477471 22.1471081,23.9843059 22.4648563,23.7872765 C22.7826044,23.5900412 23.4577156,23.1957765 23.7356415,22.5649529 C24.0139822,21.9343353 24.0139822,21.6189235 24.4505748,20.7908647 C24.8873748,19.9630118 24.7685304,19.3715118 24.9668119,18.7406882 C25.1655081,18.1098647 25.6433748,17.6760706 26.29816,16.9666 C26.9527378,16.2571294 26.8349304,15.8626588 26.29816,15.9019824 C25.7613896,15.9413059 25.4034044,15.4290706 25.1254785,14.7982471 C24.8479674,14.1672176 24.5302193,12.5903647 24.7685304,13.1028059 C25.0068415,13.6156588 25.4832563,15.0348059 25.9598785,15.2713647 C26.4365007,15.5079235 27.3105156,14.8692765 27.5090044,14.6366294 C27.7074933,14.4037765 27.6280563,13.7338353 27.1116119,13.4577471 C26.5953748,13.1816588 26.913123,12.9453059 27.4295674,13.2605118 C27.9458044,13.5759235 28.5416859,13.8913353 28.4224267,13.2213941 Z" id="Stroke-34" stroke="#2196F3" mask="url(#mask-25)"></path>
|
|
||||||
</g>
|
|
||||||
<g id="Group-39">
|
|
||||||
<mask id="mask-27" fill="white">
|
|
||||||
<use xlink:href="#path-26"></use>
|
|
||||||
</mask>
|
|
||||||
<g id="Clip-38"></g>
|
|
||||||
<path d="M18.8503674,6.59758235 C18.6070785,6.67025882 18.4135674,7.14955294 18.5326193,7.38631765 C18.6520859,7.62267059 18.4135674,7.26772941 18.0958193,7.46496471 C17.7778637,7.66199412 17.3060119,8.21417059 17.7405304,8.25328824 C18.1752563,8.29302353 18.0958193,7.93808235 18.4930044,8.09578824 C18.8901896,8.25328824 19.3269896,8.21417059 19.3269896,7.81990588 C19.3269896,7.42543529 19.2475526,6.47940588 18.8503674,6.59758235" id="Fill-37" fill="#2196F3" mask="url(#mask-27)"></path>
|
|
||||||
</g>
|
|
||||||
<g id="Group-42">
|
|
||||||
<mask id="mask-29" fill="white">
|
|
||||||
<use xlink:href="#path-28"></use>
|
|
||||||
</mask>
|
|
||||||
<g id="Clip-41"></g>
|
|
||||||
<path d="M18.8503674,6.59758235 C18.6070785,6.67025882 18.4135674,7.14955294 18.5326193,7.38631765 C18.6520859,7.62267059 18.4135674,7.26772941 18.0958193,7.46496471 C17.7778637,7.66199412 17.3060119,8.21417059 17.7405304,8.25328824 C18.1752563,8.29302353 18.0958193,7.93808235 18.4930044,8.09578824 C18.8901896,8.25328824 19.3269896,8.21417059 19.3269896,7.81990588 C19.3269896,7.42543529 19.2475526,6.47940588 18.8503674,6.59758235 Z" id="Stroke-40" stroke="#2196F3" mask="url(#mask-29)"></path>
|
|
||||||
</g>
|
|
||||||
<g id="Group-45">
|
|
||||||
<mask id="mask-31" fill="white">
|
|
||||||
<use xlink:href="#path-30"></use>
|
|
||||||
</mask>
|
|
||||||
<g id="Clip-44"></g>
|
|
||||||
<path d="M16.2689333,4.78404706 C16.0295852,4.81801765 15.9509778,5.41487059 16.4674222,5.49372353 C16.9838667,5.57257647 17.9082815,5.37534118 17.5652296,5.05992941 C17.2221778,4.74472353 16.8252,4.70519412 16.2689333,4.78404706" id="Fill-43" fill="#2196F3" mask="url(#mask-31)"></path>
|
|
||||||
</g>
|
|
||||||
<g id="Group-48">
|
|
||||||
<mask id="mask-33" fill="white">
|
|
||||||
<use xlink:href="#path-32"></use>
|
|
||||||
</mask>
|
|
||||||
<g id="Clip-47"></g>
|
|
||||||
<path d="M16.2689333,4.78404706 C16.0295852,4.81801765 15.9509778,5.41487059 16.4674222,5.49372353 C16.9838667,5.57257647 17.9082815,5.37534118 17.5652296,5.05992941 C17.2221778,4.74472353 16.8252,4.70519412 16.2689333,4.78404706 Z" id="Stroke-46" stroke="#2196F3" mask="url(#mask-33)"></path>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<g id="Group-54">
|
|
||||||
<path d="M28.1296528,14.2858085 C28.1296528,21.8560338 21.992681,27.9932099 14.4222514,27.9932099 C6.85182183,27.9932099 0.71485,21.8560338 0.71485,14.2858085 C0.71485,6.71537887 6.85182183,0.578407042 14.4222514,0.578407042 C21.992681,0.578407042 28.1296528,6.71537887 28.1296528,14.2858085 Z" id="Stroke-50" stroke="#2196F3"></path>
|
|
||||||
<path d="M14.4854387,1.69603028 C7.4461993,1.69603028 1.71931197,7.42291761 1.71931197,14.462157 C1.71931197,21.5013965 7.4461993,27.2280796 14.4854387,27.2280796 C21.5246782,27.2280796 27.2513613,21.5013965 27.2513613,14.462157 C27.2513613,7.42291761 21.5246782,1.69603028 14.4854387,1.69603028 M14.4854387,28.8618824 C6.54536127,28.8618824 0.0855091549,22.4022345 0.0855091549,14.462157 C0.0855091549,6.52187535 6.54536127,0.0622274648 14.4854387,0.0622274648 C22.4255162,0.0622274648 28.8851641,6.52187535 28.8851641,14.462157 C28.8851641,22.4022345 22.4255162,28.8618824 14.4854387,28.8618824" id="Fill-52" fill="#2196F3"></path>
|
|
||||||
</g>
|
|
||||||
<g id="Group-64" transform="translate(16.000000, 0.000000)">
|
|
||||||
<g id="Group-57">
|
|
||||||
<mask id="mask-35" fill="white">
|
|
||||||
<use xlink:href="#path-34"></use>
|
|
||||||
</mask>
|
|
||||||
<g id="Clip-56"></g>
|
|
||||||
<path d="M7.92792,7.13868 C7.61952,7.44688 7.25072,7.60088 6.82132,7.60088 C6.39172,7.60088 6.02312,7.44688 5.71492,7.13868 C5.40672,6.83048 5.25232,6.46128 5.25232,6.03208 C5.25232,5.60268 5.40672,5.23948 5.71492,4.94208 C6.02312,4.64488 6.39172,4.49608 6.82132,4.49608 C7.25072,4.49608 7.61952,4.64488 7.92792,4.94208 C8.23592,5.23948 8.39032,5.60268 8.39032,6.03208 C8.39032,6.46128 8.23592,6.83048 7.92792,7.13868 M10.43812,2.53088 C9.44732,1.55088 8.24732,1.06088 6.83792,1.06088 C5.42852,1.06088 4.22852,1.55088 3.23752,2.53088 C2.24652,3.51048 1.75112,4.69428 1.75112,6.08168 C1.75112,6.91828 2.17492,8.31688 3.02292,10.27648 C3.87052,12.23628 4.71832,14.01988 5.56592,15.62728 C6.41412,17.23468 6.83792,18.01648 6.83792,17.97268 C6.96992,17.73048 7.15152,17.40008 7.38272,16.98168 C7.61412,16.56328 8.02152,15.77608 8.60512,14.61988 C9.18852,13.46368 9.70052,12.40108 10.14052,11.43288 C10.58132,10.46368 10.98872,9.46168 11.36292,8.42648 C11.73752,7.39188 11.92432,6.61008 11.92432,6.08168 C11.92432,4.69428 11.42892,3.51048 10.43812,2.53088" id="Fill-55" fill="#FFFFFF" mask="url(#mask-35)"></path>
|
|
||||||
</g>
|
|
||||||
<g id="Group-60">
|
|
||||||
<mask id="mask-37" fill="white">
|
|
||||||
<use xlink:href="#path-36"></use>
|
|
||||||
</mask>
|
|
||||||
<g id="Clip-59"></g>
|
|
||||||
<path d="M7.92792,7.13868 C7.61952,7.44688 7.25072,7.60088 6.82132,7.60088 C6.39172,7.60088 6.02312,7.44688 5.71492,7.13868 C5.40672,6.83048 5.25232,6.46128 5.25232,6.03208 C5.25232,5.60268 5.40672,5.23948 5.71492,4.94208 C6.02312,4.64488 6.39172,4.49608 6.82132,4.49608 C7.25072,4.49608 7.61952,4.64488 7.92792,4.94208 C8.23592,5.23948 8.39032,5.60268 8.39032,6.03208 C8.39032,6.46128 8.23592,6.83048 7.92792,7.13868 M10.43812,2.53088 C9.44732,1.55088 8.24732,1.06088 6.83792,1.06088 C5.42852,1.06088 4.22852,1.55088 3.23752,2.53088 C2.24652,3.51048 1.75112,4.69428 1.75112,6.08168 C1.75112,6.91828 2.17492,8.31688 3.02292,10.27648 C3.87052,12.23628 4.71832,14.01988 5.56592,15.62728 C6.41412,17.23468 6.83792,18.01648 6.83792,17.97268 C6.96992,17.73048 7.15152,17.40008 7.38272,16.98168 C7.61412,16.56328 8.02152,15.77608 8.60512,14.61988 C9.18852,13.46368 9.70052,12.40108 10.14052,11.43288 C10.58132,10.46368 10.98872,9.46168 11.36292,8.42648 C11.73752,7.39188 11.92432,6.61008 11.92432,6.08168 C11.92432,4.69428 11.42892,3.51048 10.43812,2.53088" id="Fill-58" fill="#FFFFFF" mask="url(#mask-37)"></path>
|
|
||||||
</g>
|
|
||||||
<g id="Group-63">
|
|
||||||
<mask id="mask-39" fill="white">
|
|
||||||
<use xlink:href="#path-38"></use>
|
|
||||||
</mask>
|
|
||||||
<g id="Clip-62"></g>
|
|
||||||
<path d="M6.82128,4.9961 C6.51968,4.9961 6.27828,5.0933 6.06188,5.3021 C5.85068,5.5057 5.75228,5.7377 5.75228,6.0321 C5.75228,6.3301 5.85268,6.5695 6.06828,6.7851 C6.28368,7.0005 6.52308,7.1009 6.82128,7.1009 C7.11948,7.1009 7.35888,7.0005 7.57448,6.7849 C7.78988,6.5695 7.89028,6.3301 7.89028,6.0321 C7.89028,5.7377 7.79188,5.5057 7.58068,5.3017 C7.36428,5.0933 7.12288,4.9961 6.82128,4.9961 L6.82128,4.9961 Z M6.82128,8.1009 C6.25648,8.1009 5.76528,7.8961 5.36128,7.4921 C4.95728,7.0881 4.75228,6.5969 4.75228,6.0321 C4.75228,5.4639 4.95928,4.9761 5.36748,4.5823 C5.77108,4.1933 6.26008,3.9961 6.82128,3.9961 C7.38228,3.9961 7.87128,4.1933 8.27488,4.5821 C8.68348,4.9763 8.89028,5.4641 8.89028,6.0321 C8.89028,6.5967 8.68548,7.0879 8.28168,7.4921 C7.87728,7.8961 7.38608,8.1009 6.82128,8.1009 L6.82128,8.1009 Z M6.83788,1.5609 C5.55368,1.5609 4.49108,1.9945 3.58908,2.8865 C2.68888,3.7763 2.25108,4.8215 2.25108,6.0817 C2.25108,6.6073 2.46468,7.7273 3.48188,10.0779 C4.31968,12.0155 5.16968,13.8041 6.00828,15.3941 C6.38528,16.1089 6.64868,16.6035 6.83208,16.9447 C6.86828,16.8789 6.90608,16.8105 6.94528,16.7399 C7.17208,16.3295 7.58028,15.5405 8.15888,14.3947 C8.73768,13.2469 9.25128,12.1809 9.68528,11.2259 C10.11888,10.2727 10.52508,9.2735 10.89248,8.2565 C11.33228,7.0427 11.42448,6.4039 11.42448,6.0817 C11.42448,4.8215 10.98668,3.7765 10.08648,2.8865 C9.18468,1.9945 8.12208,1.5609 6.83788,1.5609 L6.83788,1.5609 Z M7.04328,18.4751 L6.83508,18.4745 C6.52208,18.4733 6.46988,18.3767 6.06328,17.6257 C5.85068,17.2335 5.53768,16.6451 5.12388,15.8607 C4.27328,14.2479 3.41208,12.4359 2.56388,10.4749 C1.68068,8.4333 1.25108,6.9963 1.25108,6.0817 C1.25108,4.5619 1.80108,3.2475 2.88588,2.1753 C3.96928,1.1041 5.29908,0.5609 6.83788,0.5609 C8.37688,0.5609 9.70648,1.1041 10.78968,2.1753 C11.87448,3.2477 12.42448,4.5621 12.42448,6.0817 C12.42448,6.6751 12.23108,7.4977 11.83308,8.5967 C11.45648,9.6383 11.04028,10.6623 10.59568,11.6399 C10.15628,12.6071 9.63668,13.6855 9.05148,14.8451 C8.45908,16.0185 8.05648,16.7965 7.82048,17.2235 C7.62348,17.5797 7.46268,17.8721 7.33788,18.1003 L7.33788,18.1803 L7.04328,18.4751 Z" id="Fill-61" fill="#2196F3" mask="url(#mask-39)"></path>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<g id="Translation-Manageme" transform="translate(40.000000, 6.000000)" fill-rule="nonzero">
|
|
||||||
<path d="M2.688,9.94 L2.688,1.946 L1.42108547e-14,1.946 L1.42108547e-14,0.504 L7.126,0.504 L7.126,1.946 L4.41,1.946 L4.41,9.94 L2.688,9.94 Z M7.462,9.94 L7.462,5.32 C7.462,4.396 7.448,3.724 7.406,3.122 L8.89,3.122 L8.96,4.41 L9.002,4.41 C9.338,3.458 10.136,2.968 10.878,2.968 C11.046,2.968 11.144,2.982 11.284,3.01 L11.284,4.62 C11.13,4.592 10.976,4.578 10.766,4.578 C9.954,4.578 9.38,5.096 9.226,5.88 C9.198,6.034 9.184,6.216 9.184,6.398 L9.184,9.94 L7.462,9.94 Z M17.612,5.852 L17.612,8.302 C17.612,8.918 17.64,9.52 17.724,9.94 L16.17,9.94 L16.044,9.184 L16.002,9.184 C15.596,9.716 14.882,10.094 13.986,10.094 C12.614,10.094 11.844,9.1 11.844,8.064 C11.844,6.356 13.37,5.474 15.89,5.488 L15.89,5.376 C15.89,4.928 15.708,4.186 14.504,4.186 C13.832,4.186 13.132,4.396 12.67,4.69 L12.334,3.57 C12.838,3.262 13.72,2.968 14.798,2.968 C16.982,2.968 17.612,4.354 17.612,5.852 Z M15.932,7.546 L15.932,6.608 C14.714,6.594 13.552,6.846 13.552,7.882 C13.552,8.554 13.986,8.862 14.532,8.862 C15.232,8.862 15.722,8.414 15.89,7.924 C15.932,7.798 15.932,7.672 15.932,7.546 Z M19.432,9.94 L19.432,5.152 C19.432,4.368 19.404,3.71 19.376,3.122 L20.874,3.122 L20.958,4.144 L21,4.144 C21.308,3.612 22.05,2.968 23.184,2.968 C24.374,2.968 25.606,3.738 25.606,5.894 L25.606,9.94 L23.884,9.94 L23.884,6.09 C23.884,5.11 23.52,4.368 22.582,4.368 C21.896,4.368 21.42,4.858 21.238,5.376 C21.182,5.516 21.154,5.726 21.154,5.922 L21.154,9.94 L19.432,9.94 Z M27.02,9.604 L27.37,8.344 C27.748,8.582 28.49,8.848 29.106,8.848 C29.862,8.848 30.198,8.526 30.198,8.078 C30.198,7.616 29.918,7.392 29.078,7.084 C27.748,6.636 27.202,5.908 27.202,5.096 C27.202,3.892 28.196,2.968 29.778,2.968 C30.534,2.968 31.192,3.15 31.598,3.374 L31.248,4.578 C30.968,4.41 30.408,4.186 29.806,4.186 C29.19,4.186 28.854,4.494 28.854,4.914 C28.854,5.348 29.176,5.544 30.044,5.866 C31.29,6.3 31.85,6.93 31.864,7.938 C31.864,9.184 30.898,10.08 29.092,10.08 C28.266,10.08 27.524,9.884 27.02,9.604 Z M33.278,9.94 L33.278,-7.10542736e-15 L35,-7.10542736e-15 L35,9.94 L33.278,9.94 Z M42.168,5.852 L42.168,8.302 C42.168,8.918 42.196,9.52 42.28,9.94 L40.726,9.94 L40.6,9.184 L40.558,9.184 C40.152,9.716 39.438,10.094 38.542,10.094 C37.17,10.094 36.4,9.1 36.4,8.064 C36.4,6.356 37.926,5.474 40.446,5.488 L40.446,5.376 C40.446,4.928 40.264,4.186 39.06,4.186 C38.388,4.186 37.688,4.396 37.226,4.69 L36.89,3.57 C37.394,3.262 38.276,2.968 39.354,2.968 C41.538,2.968 42.168,4.354 42.168,5.852 Z M40.488,7.546 L40.488,6.608 C39.27,6.594 38.108,6.846 38.108,7.882 C38.108,8.554 38.542,8.862 39.088,8.862 C39.788,8.862 40.278,8.414 40.446,7.924 C40.488,7.798 40.488,7.672 40.488,7.546 Z M44.184,1.848 L45.878,1.372 L45.878,3.122 L47.516,3.122 L47.516,4.41 L45.878,4.41 L45.878,7.42 C45.878,8.26 46.102,8.68 46.76,8.68 C47.068,8.68 47.236,8.666 47.432,8.61 L47.46,9.912 C47.208,10.01 46.746,10.094 46.214,10.094 C45.57,10.094 45.052,9.884 44.73,9.534 C44.352,9.142 44.184,8.512 44.184,7.616 L44.184,4.41 L43.218,4.41 L43.218,3.122 L44.184,3.122 L44.184,1.848 Z M50.54,9.94 L48.818,9.94 L48.818,3.122 L50.54,3.122 L50.54,9.94 Z M49.686,0.28 C50.274,0.28 50.638,0.686 50.652,1.218 C50.652,1.722 50.274,2.142 49.672,2.142 C49.098,2.142 48.72,1.722 48.72,1.218 C48.72,0.686 49.112,0.28 49.686,0.28 Z M55.496,2.968 C57.526,2.968 58.884,4.396 58.884,6.468 C58.884,8.974 57.12,10.094 55.384,10.094 C53.466,10.094 51.968,8.778 51.968,6.58 C51.968,4.34 53.438,2.968 55.496,2.968 Z M55.454,4.214 C54.236,4.214 53.746,5.404 53.746,6.538 C53.746,7.854 54.404,8.848 55.44,8.848 C56.42,8.848 57.106,7.896 57.106,6.51 C57.106,5.446 56.63,4.214 55.454,4.214 Z M60.298,9.94 L60.298,5.152 C60.298,4.368 60.27,3.71 60.242,3.122 L61.74,3.122 L61.824,4.144 L61.866,4.144 C62.174,3.612 62.916,2.968 64.05,2.968 C65.24,2.968 66.472,3.738 66.472,5.894 L66.472,9.94 L64.75,9.94 L64.75,6.09 C64.75,5.11 64.386,4.368 63.448,4.368 C62.762,4.368 62.286,4.858 62.104,5.376 C62.048,5.516 62.02,5.726 62.02,5.922 L62.02,9.94 L60.298,9.94 Z M79.422,9.94 L79.24,6.09 C79.184,4.872 79.114,3.402 79.128,2.128 L79.086,2.128 C78.778,3.276 78.386,4.55 77.966,5.74 L76.58,9.828 L75.264,9.828 L74.004,5.796 C73.64,4.592 73.304,3.304 73.052,2.128 L73.024,2.128 C72.982,3.36 72.926,4.858 72.856,6.146 L72.646,9.94 L71.036,9.94 L71.666,0.504 L73.934,0.504 L75.166,4.298 C75.502,5.404 75.796,6.524 76.034,7.574 L76.076,7.574 C76.328,6.552 76.65,5.39 77.014,4.284 L78.316,0.504 L80.556,0.504 L81.102,9.94 L79.422,9.94 Z M88.13,5.852 L88.13,8.302 C88.13,8.918 88.158,9.52 88.242,9.94 L86.688,9.94 L86.562,9.184 L86.52,9.184 C86.114,9.716 85.4,10.094 84.504,10.094 C83.132,10.094 82.362,9.1 82.362,8.064 C82.362,6.356 83.888,5.474 86.408,5.488 L86.408,5.376 C86.408,4.928 86.226,4.186 85.022,4.186 C84.35,4.186 83.65,4.396 83.188,4.69 L82.852,3.57 C83.356,3.262 84.238,2.968 85.316,2.968 C87.5,2.968 88.13,4.354 88.13,5.852 Z M86.45,7.546 L86.45,6.608 C85.232,6.594 84.07,6.846 84.07,7.882 C84.07,8.554 84.504,8.862 85.05,8.862 C85.75,8.862 86.24,8.414 86.408,7.924 C86.45,7.798 86.45,7.672 86.45,7.546 Z M89Line truncated
|
|
||||||
<path d="M0.91,27.712 L0.91,20.376 C0.91,19.508 0.882,18.808 0.854,18.164 L1.946,18.164 L2.016,19.326 L2.044,19.326 C2.534,18.5 3.346,18.024 4.452,18.024 C6.104,18.024 7.336,19.41 7.336,21.454 C7.336,23.89 5.838,25.094 4.242,25.094 C3.346,25.094 2.562,24.702 2.156,24.03 L2.128,24.03 L2.128,27.712 L0.91,27.712 Z M2.128,21.006 L2.128,22.182 C2.128,22.364 2.142,22.532 2.184,22.686 C2.408,23.54 3.15,24.128 4.032,24.128 C5.334,24.128 6.09,23.064 6.09,21.51 C6.09,20.166 5.376,19.004 4.074,19.004 C3.234,19.004 2.436,19.592 2.212,20.516 C2.17,20.67 2.128,20.852 2.128,21.006 Z M11.746,18.024 C13.692,18.024 15.008,19.438 15.008,21.496 C15.008,24.002 13.258,25.094 11.634,25.094 C9.814,25.094 8.386,23.75 8.386,21.608 C8.386,19.354 9.884,18.024 11.746,18.024 Z M11.718,18.948 C10.276,18.948 9.646,20.292 9.646,21.566 C9.646,23.05 10.486,24.17 11.69,24.17 C12.866,24.17 13.748,23.064 13.748,21.538 C13.748,20.39 13.174,18.948 11.718,18.948 Z M15.694,18.164 L16.954,18.164 L17.864,21.608 C18.046,22.364 18.228,23.064 18.354,23.764 L18.396,23.764 C18.55,23.078 18.774,22.35 18.998,21.622 L20.104,18.164 L21.14,18.164 L22.19,21.552 C22.442,22.364 22.638,23.078 22.792,23.764 L22.834,23.764 C22.946,23.078 23.128,22.364 23.352,21.566 L24.318,18.164 L25.536,18.164 L23.352,24.94 L22.232,24.94 L21.196,21.706 C20.958,20.95 20.762,20.278 20.594,19.48 L20.566,19.48 C20.398,20.292 20.188,20.992 19.95,21.72 L18.858,24.94 L17.738,24.94 L15.694,18.164 Z M32.074,21.776 L27.314,21.776 C27.342,23.442 28.392,24.128 29.638,24.128 C30.52,24.128 31.066,23.974 31.514,23.778 L31.738,24.66 C31.304,24.856 30.548,25.094 29.47,25.094 C27.384,25.094 26.138,23.708 26.138,21.664 C26.138,19.62 27.342,18.024 29.316,18.024 C31.542,18.024 32.116,19.956 32.116,21.202 C32.116,21.454 32.102,21.636 32.074,21.776 Z M27.328,20.894 L30.926,20.894 C30.94,20.124 30.604,18.906 29.218,18.906 C27.958,18.906 27.426,20.04 27.328,20.894 Z M33.642,24.94 L33.642,20.278 C33.642,19.48 33.628,18.794 33.586,18.164 L34.664,18.164 L34.72,19.508 L34.762,19.508 C35.07,18.598 35.826,18.024 36.652,18.024 C36.778,18.024 36.876,18.038 36.988,18.052 L36.988,19.214 C36.862,19.186 36.736,19.186 36.568,19.186 C35.7,19.186 35.084,19.83 34.916,20.754 C34.888,20.922 34.874,21.132 34.874,21.328 L34.874,24.94 L33.642,24.94 Z M43.526,21.776 L38.766,21.776 C38.794,23.442 39.844,24.128 41.09,24.128 C41.972,24.128 42.518,23.974 42.966,23.778 L43.19,24.66 C42.756,24.856 42,25.094 40.922,25.094 C38.836,25.094 37.59,23.708 37.59,21.664 C37.59,19.62 38.794,18.024 40.768,18.024 C42.994,18.024 43.568,19.956 43.568,21.202 C43.568,21.454 43.554,21.636 43.526,21.776 Z M38.78,20.894 L42.378,20.894 C42.392,20.124 42.056,18.906 40.67,18.906 C39.41,18.906 38.878,20.04 38.78,20.894 Z M49.714,15 L50.932,15 L50.932,23.19 C50.932,23.792 50.96,24.478 50.988,24.94 L49.896,24.94 L49.84,23.764 L49.798,23.764 C49.434,24.52 48.622,25.094 47.516,25.094 C45.878,25.094 44.604,23.708 44.604,21.65 C44.59,19.396 46.004,18.024 47.642,18.024 C48.692,18.024 49.378,18.514 49.686,19.046 L49.714,19.046 L49.714,15 Z M49.714,22.098 L49.714,20.922 C49.714,20.768 49.7,20.558 49.658,20.404 C49.476,19.634 48.804,18.99 47.88,18.99 C46.606,18.99 45.85,20.11 45.85,21.594 C45.85,22.966 46.536,24.1 47.852,24.1 C48.678,24.1 49.434,23.54 49.658,22.63 C49.7,22.462 49.714,22.294 49.714,22.098 Z M55.958,23.19 L55.958,15 L57.176,15 L57.176,19.256 L57.204,19.256 C57.638,18.5 58.422,18.024 59.514,18.024 C61.208,18.024 62.384,19.424 62.384,21.468 C62.384,23.89 60.844,25.094 59.332,25.094 C58.352,25.094 57.568,24.716 57.05,23.834 L57.022,23.834 L56.952,24.94 L55.902,24.94 C55.93,24.478 55.958,23.792 55.958,23.19 Z M57.176,21.02 L57.176,22.224 C57.176,22.378 57.19,22.532 57.232,22.672 C57.456,23.526 58.184,24.114 59.08,24.114 C60.382,24.114 61.138,23.064 61.138,21.51 C61.138,20.152 60.438,18.99 59.108,18.99 C58.282,18.99 57.498,19.578 57.246,20.502 C57.204,20.656 57.176,20.824 57.176,21.02 Z M62.93,18.164 L64.274,18.164 L65.744,22.168 C65.912,22.616 66.08,23.148 66.192,23.554 L66.22,23.554 C66.346,23.148 66.486,22.63 66.654,22.14 L67.998,18.164 L69.3,18.164 L67.452,22.994 C66.57,25.318 65.968,26.494 65.128,27.236 C64.512,27.754 63.924,27.964 63.616,28.02 L63.308,26.998 C63.616,26.9 64.022,26.704 64.386,26.396 C64.722,26.13 65.128,25.654 65.422,25.024 C65.478,24.898 65.52,24.8 65.52,24.73 C65.52,24.66 65.492,24.562 65.422,24.408 L62.93,18.164 Z" id="poweredby" fill="#AAAAAA"></path>
|
|
||||||
<path d="M73.29,24.94 L73.29,15 L75.012,15 L75.012,24.94 L73.29,24.94 Z M79.982,17.968 C82.012,17.968 83.37,19.396 83.37,21.468 C83.37,23.974 81.606,25.094 79.87,25.094 C77.952,25.094 76.454,23.778 76.454,21.58 C76.454,19.34 77.924,17.968 79.982,17.968 Z M79.94,19.214 C78.722,19.214 78.232,20.404 78.232,21.538 C78.232,22.854 78.89,23.848 79.926,23.848 C80.906,23.848 81.592,22.896 81.592,21.51 C81.592,20.446 81.116,19.214 79.94,19.214 Z M89.558,23.456 L89.796,24.73 C89.432,24.898 88.718,25.08 87.864,25.08 C85.736,25.08 84.35,23.708 84.35,21.594 C84.35,19.55 85.75,17.968 88.144,17.968 C88.774,17.968 89.418,18.108 89.81,18.276 L89.502,19.578 C89.222,19.438 88.816,19.312 88.2,19.312 C86.884,19.312 86.1,20.278 86.114,21.524 C86.114,22.924 87.024,23.722 88.2,23.722 C88.788,23.722 89.222,23.596 89.558,23.456 Z M92.792,24.94 L91.07,24.94 L91.07,18.122 L92.792,18.122 L92.792,24.94 Z M91.938,15.28 C92.526,15.28 92.89,15.686 92.904,16.218 C92.904,16.722 92.526,17.142 91.924,17.142 C91.35,17.142 90.972,16.722 90.972,16.218 C90.972,15.686 91.364,15.28 91.938,15.28 Z M94.01,24.94 L94.01,23.946 L96.572,20.67 C96.922,20.25 97.216,19.914 97.566,19.522 L97.566,19.494 L94.262,19.494 L94.262,18.122 L99.666,18.122 L99.666,19.172 L97.174,22.378 C96.838,22.784 96.516,23.176 96.166,23.54 L96.166,23.568 L99.75,23.568 L99.75,24.94 L94.01,24.94 Z M106.638,22.014 L102.074,22.014 C102.116,23.246 103.068,23.778 104.174,23.778 C104.972,23.778 105.532,23.652 106.064,23.47 L106.316,24.646 C105.728,24.898 104.916,25.08 103.936,25.08 C101.724,25.08 100.422,23.722 100.422,21.636 C100.422,19.746 101.57,17.968 103.754,17.968 C105.966,17.968 106.694,19.788 106.694,21.286 C106.694,21.608 106.666,21.86 106.638,22.014 Z M102.074,20.81 L105.07,20.81 C105.084,20.18 104.804,19.144 103.656,19.144 C102.592,19.144 102.144,20.11 102.074,20.81 Z M108.724,25.094 C108.108,25.094 107.674,24.618 107.674,23.988 C107.674,23.344 108.122,22.882 108.738,22.882 C109.368,22.882 109.802,23.33 109.802,23.988 C109.802,24.618 109.382,25.094 108.724,25.094 Z M115.962,23.456 L116.2,24.73 C115.836,24.898 115.122,25.08 114.268,25.08 C112.14,25.08 110.754,23.708 110.754,21.594 C110.754,19.55 112.154,17.968 114.548,17.968 C115.178,17.968 115.822,18.108 116.214,18.276 L115.906,19.578 C115.626,19.438 115.22,19.312 114.604,19.312 C113.288,19.312 112.504,20.278 112.518,21.524 C112.518,22.924 113.428,23.722 114.604,23.722 C115.192,23.722 115.626,23.596 115.962,23.456 Z M120.442,17.968 C122.472,17.968 123.83,19.396 123.83,21.468 C123.83,23.974 122.066,25.094 120.33,25.094 C118.412,25.094 116.914,23.778 116.914,21.58 C116.914,19.34 118.384,17.968 120.442,17.968 Z M120.4,19.214 C119.182,19.214 118.692,20.404 118.692,21.538 C118.692,22.854 119.35,23.848 120.386,23.848 C121.366,23.848 122.052,22.896 122.052,21.51 C122.052,20.446 121.576,19.214 120.4,19.214 Z M125.244,24.94 L125.244,20.152 C125.244,19.368 125.216,18.71 125.188,18.122 L126.658,18.122 L126.742,19.13 L126.784,19.13 C127.106,18.598 127.722,17.968 128.884,17.968 C129.78,17.968 130.48,18.472 130.774,19.228 L130.802,19.228 C131.04,18.85 131.32,18.57 131.628,18.374 C132.02,18.108 132.454,17.968 133.028,17.968 C134.148,17.968 135.296,18.738 135.296,20.922 L135.296,24.94 L133.616,24.94 L133.616,21.174 C133.616,20.04 133.224,19.368 132.398,19.368 C131.81,19.368 131.376,19.788 131.194,20.278 C131.138,20.432 131.11,20.656 131.11,20.852 L131.11,24.94 L129.43,24.94 L129.43,20.992 C129.43,20.04 129.052,19.368 128.254,19.368 C127.61,19.368 127.176,19.872 127.008,20.334 C126.952,20.516 126.924,20.712 126.924,20.908 L126.924,24.94 L125.244,24.94 Z" id="locize.com" fill="#888888"></path>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 371 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 40 KiB |
@@ -1,194 +0,0 @@
|
|||||||
## Set up a local DICOM server
|
|
||||||
|
|
||||||
- [Set up a local DICOM server](#set-up-a-local-dicom-server)
|
|
||||||
- [Requirements](#requirements)
|
|
||||||
- [Open Source DICOM Image Archives](#open-source-dicom-image-archives)
|
|
||||||
- [Running Orthanc](#running-orthanc)
|
|
||||||
- [Orthanc: Learn More](#orthanc-learn-more)
|
|
||||||
- [Connecting to Orthanc](#connecting-to-orthanc)
|
|
||||||
- [Configuration: Learn More](#configuration-learn-more)
|
|
||||||
- [Running DCM4CHEE](#running-dcm4chee)
|
|
||||||
|
|
||||||
|
|
||||||
ATTENTION! Already have a remote or local server? Skip to the
|
|
||||||
[configuration section](#configuration-learn-more) below.
|
|
||||||
|
|
||||||
While the OHIF Viewer can work with any data source, the easiest to configure
|
|
||||||
are the ones that follow the [DICOMWeb][dicom-web] spec.
|
|
||||||
|
|
||||||
1. Choose and install an Image Archive
|
|
||||||
2. Upload data to your archive (e.g. with DCMTK's [storescu][storescu] or your
|
|
||||||
archive's web interface)
|
|
||||||
3. Keep the server running
|
|
||||||
|
|
||||||
For our purposes, we will be using `Orthanc`, but you can see a list of
|
|
||||||
[other Open Source options](#open-source-dicom-image-archives) below.
|
|
||||||
|
|
||||||
### Requirements
|
|
||||||
|
|
||||||
- Docker
|
|
||||||
- [Docker for Mac](https://docs.docker.com/docker-for-mac/)
|
|
||||||
- [Docker for Windows (recommended)](https://docs.docker.com/docker-for-windows/)
|
|
||||||
- [Docker Toolbox for Windows](https://docs.docker.com/toolbox/toolbox_install_windows/)
|
|
||||||
|
|
||||||
_Not sure if you have `docker` installed already? Try running `docker --version`
|
|
||||||
in command prompt or terminal_
|
|
||||||
|
|
||||||
> If you are using `Docker Toolbox` you need to change the _PROXY_DOMAIN_
|
|
||||||
> parameter in _platform/viewer/package.json_ to http://192.168.99.100:8042 or
|
|
||||||
> the ip docker-machine ip throws. This is the value [`WebPack`][webpack-proxy]
|
|
||||||
> uses to proxy requests
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Open Source DICOM Image Archives
|
|
||||||
|
|
||||||
There are a lot of options available to you to use as a local DICOM server. Here are some of the more popular ones:
|
|
||||||
|
|
||||||
| Archive | Installation |
|
|
||||||
| --------------------------------------------- | ---------------------------------- |
|
|
||||||
| [DCM4CHEE Archive 5.x][dcm4chee] | [W/ Docker][dcm4chee-docker] |
|
|
||||||
| [Orthanc][orthanc] | [W/ Docker][orthanc-docker] |
|
|
||||||
| [DICOMcloud][dicomcloud] (**DICOM Web only**) | [Installation][dicomcloud-install] |
|
|
||||||
| [OsiriX][osirix] (**Mac OSX only**) | Desktop Client |
|
|
||||||
| [Horos][horos] (**Mac OSX only**) | Desktop Client |
|
|
||||||
|
|
||||||
_Feel free to make a Pull Request if you want to add to this list._
|
|
||||||
|
|
||||||
|
|
||||||
Below, we will focus on `DCM4CHEE` and `Orthanc` usage:
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Running Orthanc
|
|
||||||
|
|
||||||
_Start Orthanc:_
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Runs orthanc so long as window remains open
|
|
||||||
yarn run orthanc:up
|
|
||||||
```
|
|
||||||
|
|
||||||
_Upload your first Study:_
|
|
||||||
|
|
||||||
1. Navigate to
|
|
||||||
[Orthanc's web interface](http://localhost:8042/app/explorer.html) at
|
|
||||||
`http://localhost:8042/app/explorer.html` in a web browser.
|
|
||||||
2. In the top right corner, click "Upload"
|
|
||||||
3. Click "Select files to upload..." and select one or more DICOM files
|
|
||||||
4. Click "Start the upload"
|
|
||||||
|
|
||||||
#### Orthanc: Learn More
|
|
||||||
|
|
||||||
You can see the `docker-compose.yml` file this command runs at
|
|
||||||
[`<project-root>/.docker/Nginx-Orthanc/`][orthanc-docker-compose], and more on
|
|
||||||
Orthanc for Docker in [Orthanc's documentation][orthanc-docker].
|
|
||||||
|
|
||||||
#### Connecting to Orthanc
|
|
||||||
|
|
||||||
Now that we have a local Orthanc instance up and running, we need to configure
|
|
||||||
our web application to connect to it. Open a new terminal window, navigate to
|
|
||||||
this repository's root directory, and run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# If you haven't already, enable yarn workspaces
|
|
||||||
yarn config set workspaces-experimental true
|
|
||||||
|
|
||||||
# Restore dependencies
|
|
||||||
yarn install
|
|
||||||
|
|
||||||
# Run our dev command, but with the local orthanc config
|
|
||||||
yarn run dev:orthanc
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Configuration: Learn More
|
|
||||||
|
|
||||||
> For more configuration fun, check out the
|
|
||||||
> [Essentials Configuration](./index.md) guide.
|
|
||||||
|
|
||||||
Let's take a look at what's going on under the hood here. `yarn run dev:orthanc`
|
|
||||||
is running the `dev:orthanc` script in our project's `package.json` (inside `platform/viewer`). That script
|
|
||||||
is:
|
|
||||||
|
|
||||||
```js
|
|
||||||
cross-env NODE_ENV=development PROXY_TARGET=/dicom-web PROXY_DOMAIN=http://localhost:8042 APP_CONFIG=config/docker_nginx-orthanc.js webpack-dev-server --config .webpack/webpack.pwa.js -w
|
|
||||||
```
|
|
||||||
|
|
||||||
- `cross-env` sets three environment variables
|
|
||||||
- PROXY_TARGET: `/dicom-web`
|
|
||||||
- PROXY_DOMAIN: `http://localhost:8042`
|
|
||||||
- APP_CONFIG: `config/docker_nginx-orthanc.js`
|
|
||||||
- `webpack-dev-server` runs using the `.webpack/webpack.pwa.js` configuration
|
|
||||||
file. It will watch for changes and update as we develop.
|
|
||||||
|
|
||||||
`PROXY_TARGET` and `PROXY_DOMAIN` tell our development server to proxy requests
|
|
||||||
to `Orthanc`. This allows us to bypass CORS issues that normally occur when
|
|
||||||
requesting resources that live at a different domain.
|
|
||||||
|
|
||||||
The `APP_CONFIG` value tells our app which file to load on to `window.config`.
|
|
||||||
By default, our app uses the file at
|
|
||||||
`<project-root>/platform/viewer/public/config/default.js`. Here is what that
|
|
||||||
configuration looks like:
|
|
||||||
|
|
||||||
```js
|
|
||||||
window.config = {
|
|
||||||
routerBasename: '/',
|
|
||||||
extensions: [],
|
|
||||||
modes: [],
|
|
||||||
showStudyList: true,
|
|
||||||
dataSources: [
|
|
||||||
{
|
|
||||||
friendlyName: 'dcmjs DICOMWeb Server',
|
|
||||||
namespace: 'org.ohif.default.dataSourcesModule.dicomweb',
|
|
||||||
sourceName: 'dicomweb',
|
|
||||||
configuration: {
|
|
||||||
name: 'DCM4CHEE',
|
|
||||||
wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado',
|
|
||||||
qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
|
|
||||||
wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
|
|
||||||
qidoSupportsIncludeField: true,
|
|
||||||
supportsReject: true,
|
|
||||||
imageRendering: 'wadors',
|
|
||||||
thumbnailRendering: 'wadors',
|
|
||||||
enableStudyLazyLoad: true,
|
|
||||||
supportsFuzzyMatching: true,
|
|
||||||
supportsWildcard: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
defaultDataSourceName: 'dicomweb',
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
To learn more about how you can configure the OHIF Viewer, check out our
|
|
||||||
[Configuration Guide](./index.md).
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Running DCM4CHEE
|
|
||||||
|
|
||||||
dcm4che is a collection of open source applications for healthcare enterprise written in Java programming language
|
|
||||||
which implements DICOM standard. dcm4chee (extra 'e' at the end) is dcm4che project
|
|
||||||
for an Image Manager/Image Archive which provides storage, retrieval and other functionalities.
|
|
||||||
You can read more about dcm4chee in their website [here](https://www.dcm4che.org/)
|
|
||||||
|
|
||||||
DCM4chee installation is out of scope for these tutorials and can be found [here](https://github.com/dcm4che/dcm4chee-arc-light/wiki/Run-minimum-set-of-archive-services-on-a-single-host)
|
|
||||||
|
|
||||||
An overview of steps for running OHIF Viewer using a local DCM4CHEE is shown below:
|
|
||||||
|
|
||||||
<div style="padding:56.25% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/557570043?badge=0&autopause=0&player_id=0&app_id=58479" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen style="position:absolute;top:0;left:0;width:100%;height:100%;" title="Local dcm4chee"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[dcm4chee]: https://github.com/dcm4che/dcm4chee-arc-light
|
|
||||||
[dcm4chee-docker]: https://github.com/dcm4che/dcm4chee-arc-light/wiki/Running-on-Docker
|
|
||||||
[orthanc]: https://www.orthanc-server.com/
|
|
||||||
[orthanc-docker]: http://book.orthanc-server.com/users/docker.html
|
|
||||||
[dicomcloud]: https://github.com/DICOMcloud/DICOMcloud
|
|
||||||
[dicomcloud-install]: https://github.com/DICOMcloud/DICOMcloud#running-the-code
|
|
||||||
[osirix]: http://www.osirix-viewer.com/
|
|
||||||
[horos]: https://www.horosproject.org/
|
|
||||||
[default-config]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/public/config/default.js
|
|
||||||
[html-templates]: https://github.com/OHIF/Viewers/tree/master/platform/viewer/public/html-templates
|
|
||||||
[config-files]: https://github.com/OHIF/Viewers/tree/master/platform/viewer/public/config
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
# Configuring Data Source
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
- [Configuring Data Source](#configuring-data-source)
|
|
||||||
- [Overview](#overview)
|
|
||||||
- [Configuration Files](#configuration-files)
|
|
||||||
- [Environment Variables](#environment-variables)
|
|
||||||
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
After following the steps outlined in
|
|
||||||
[Getting Started](./../development/getting-started.md), you'll notice that the
|
|
||||||
OHIF Viewer has data for several studies and their images. You didn't add this
|
|
||||||
data, so where is it coming from?
|
|
||||||
|
|
||||||
By default, the viewer is configured to connect to a remote server hosted by the
|
|
||||||
nice folks over at [dcmjs.org][dcmjs-org]. While convenient for getting started,
|
|
||||||
the time may come when you want to develop using your own data either locally or
|
|
||||||
remotely.
|
|
||||||
|
|
||||||
|
|
||||||
## Configuration Files
|
|
||||||
|
|
||||||
The configuration for our viewer is in the `<root>platform/viewer/public/config`
|
|
||||||
directory. Our build process knows which configuration file to use based on the
|
|
||||||
`APP_CONFIG` environment variable. By default, its value is
|
|
||||||
[`config/default.js`][default-config]. The majority of the viewer's features,
|
|
||||||
and registered extension's features, are configured using this file.
|
|
||||||
|
|
||||||
The simplest way is to update the existing default config:
|
|
||||||
|
|
||||||
> _/platform/viewer/public/config/default.js_
|
|
||||||
|
|
||||||
```js
|
|
||||||
window.config = {
|
|
||||||
routerBasename: '/',
|
|
||||||
extensions: [],
|
|
||||||
modes: [],
|
|
||||||
showStudyList: true,
|
|
||||||
dataSources: [
|
|
||||||
{
|
|
||||||
friendlyName: 'dcmjs DICOMWeb Server',
|
|
||||||
namespace: 'org.ohif.default.dataSourcesModule.dicomweb',
|
|
||||||
sourceName: 'dicomweb',
|
|
||||||
configuration: {
|
|
||||||
name: 'DCM4CHEE',
|
|
||||||
wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado',
|
|
||||||
qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
|
|
||||||
wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
|
|
||||||
qidoSupportsIncludeField: true,
|
|
||||||
supportsReject: true,
|
|
||||||
imageRendering: 'wadors',
|
|
||||||
thumbnailRendering: 'wadors',
|
|
||||||
enableStudyLazyLoad: true,
|
|
||||||
supportsFuzzyMatching: true,
|
|
||||||
supportsWildcard: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
defaultDataSourceName: 'dicomweb',
|
|
||||||
};
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
> As you can see a new change in `OHIF-v3` is the addition of `dataSources`.
|
|
||||||
> You can build your own datasource and map it to the internal data structure of OHIF’s > metadata and enjoy using other peoples developed mode on your own data!
|
|
||||||
>
|
|
||||||
> You can read more about data sources at [Data Source section in Modes](../modes/index.md)
|
|
||||||
>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
The configuration can also be written as a JS Function in case you need to inject dependencies like external services:
|
|
||||||
|
|
||||||
|
|
||||||
```js
|
|
||||||
window.config = ({ servicesManager } = {}) => {
|
|
||||||
const { UIDialogService } = servicesManager.services;
|
|
||||||
return {
|
|
||||||
cornerstoneExtensionConfig: {
|
|
||||||
tools: {
|
|
||||||
ArrowAnnotate: {
|
|
||||||
configuration: {
|
|
||||||
getTextCallback: (callback, eventDetails) => UIDialogService.create({...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
routerBasename: '/',
|
|
||||||
dataSources: [
|
|
||||||
{
|
|
||||||
friendlyName: 'dcmjs DICOMWeb Server',
|
|
||||||
namespace: 'org.ohif.default.dataSourcesModule.dicomweb',
|
|
||||||
sourceName: 'dicomweb',
|
|
||||||
configuration: {
|
|
||||||
name: 'DCM4CHEE',
|
|
||||||
wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado',
|
|
||||||
qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
|
|
||||||
wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
|
|
||||||
qidoSupportsIncludeField: true,
|
|
||||||
supportsReject: true,
|
|
||||||
imageRendering: 'wadors',
|
|
||||||
thumbnailRendering: 'wadors',
|
|
||||||
enableStudyLazyLoad: true,
|
|
||||||
supportsFuzzyMatching: true,
|
|
||||||
supportsWildcard: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
defaultDataSourceName: 'dicomweb',
|
|
||||||
};
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
<!-- **Embedded Use Note:**
|
|
||||||
|
|
||||||
Alternatively, when using the `umd` bundle for embedded use cases, these same
|
|
||||||
values are what you'll pass to `installViewer` method:
|
|
||||||
|
|
||||||
`OHIFStandaloneViewer.installViewer(window.config)` -->
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
We use environment variables at build and dev time to change the Viewer's
|
|
||||||
behavior. We can update the `HTML_TEMPLATE` to easily change which extensions
|
|
||||||
are registered, and specify a different `APP_CONFIG` to connect to an
|
|
||||||
alternative data source (or even specify different default hotkeys).
|
|
||||||
|
|
||||||
| Environment Variable | Description | Default |
|
|
||||||
| -------------------- | -------------------------------------------------------------------------------------------------- | ------------------- |
|
|
||||||
| `HTML_TEMPLATE` | Which [HTML template][html-templates] to use as our web app's entry point. Specific to PWA builds. | `index.html` |
|
|
||||||
| `PUBLIC_URL` | The route relative to the host that the app will be served from. Specific to PWA builds. | `/` |
|
|
||||||
| `APP_CONFIG` | Which [configuration file][config-file] to copy to output as `app-config.js` | `config/default.js` |
|
|
||||||
| `PROXY_TARGET` | When developing, proxy requests that match this pattern to `PROXY_DOMAIN` | `undefined` |
|
|
||||||
| `PROXY_DOMAIN` | When developing, proxy requests from `PROXY_TARGET` to `PROXY_DOMAIN` | `undefined` |
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
You can also create a new config file and specify its path relative to the build
|
|
||||||
output's root by setting the `APP_CONFIG` environment variable. You can set the
|
|
||||||
value of this environment variable a few different ways:
|
|
||||||
|
|
||||||
- ~[Add a temporary environment variable in your shell](https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#adding-temporary-environment-variables-in-your-shell)~
|
|
||||||
- Previous `react-scripts` functionality that we need to duplicate with
|
|
||||||
`dotenv-webpack`
|
|
||||||
- ~[Add environment specific variables in `.env` file(s)](https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#adding-development-environment-variables-in-env)~
|
|
||||||
- Previous `react-scripts` functionality that we need to duplicate with
|
|
||||||
`dotenv-webpack`
|
|
||||||
- Using the `cross-env` package in an npm script:
|
|
||||||
- `"build": "cross-env APP_CONFIG=config/my-config.js react-scripts build"`
|
|
||||||
|
|
||||||
After updating the configuration, `yarn run build` to generate updated build
|
|
||||||
output.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Links
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
|
||||||
[dcmjs-org]: https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado
|
|
||||||
[dicom-web]: https://en.wikipedia.org/wiki/DICOMweb
|
|
||||||
[storescu]: http://support.dcmtk.org/docs/storescu.html
|
|
||||||
[webpack-proxy]: https://webpack.js.org/configuration/dev-server/#devserverproxy
|
|
||||||
[orthanc-docker-compose]: https://github.com/OHIF/Viewers/tree/master/.docker/Nginx-Orthanc
|
|
||||||
<!-- Archives -->
|
|
||||||
[dcm4chee]: https://github.com/dcm4che/dcm4chee-arc-light
|
|
||||||
[dcm4chee-docker]: https://github.com/dcm4che/dcm4chee-arc-light/wiki/Running-on-Docker
|
|
||||||
[orthanc]: https://www.orthanc-server.com/
|
|
||||||
[orthanc-docker]: http://book.orthanc-server.com/users/docker.html
|
|
||||||
[dicomcloud]: https://github.com/DICOMcloud/DICOMcloud
|
|
||||||
[dicomcloud-install]: https://github.com/DICOMcloud/DICOMcloud#running-the-code
|
|
||||||
[osirix]: http://www.osirix-viewer.com/
|
|
||||||
[horos]: https://www.horosproject.org/
|
|
||||||
[default-config]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/public/config/default.js
|
|
||||||
[html-templates]: https://github.com/OHIF/Viewers/tree/master/platform/viewer/public/html-templates
|
|
||||||
[config-files]: https://github.com/OHIF/Viewers/tree/master/platform/viewer/public/config
|
|
||||||
<!-- prettier-ignore-end -->
|
|
||||||
@@ -1,302 +0,0 @@
|
|||||||
# Deployment
|
|
||||||
|
|
||||||
The OHIF Viewer can be embedded in other web applications via it's [packaged
|
|
||||||
script source][viewer-npm], or served up as a stand-alone PWA ([progressive web
|
|
||||||
application][pwa-url]) by building and hosting a collection of static assets. In
|
|
||||||
either case, you will need to configure your instance of the Viewer so that it
|
|
||||||
can connect to your data source (the database or PACS that provides the data
|
|
||||||
your Viewer will display).
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Our goal is to make deployment as simple and painless as possible; however,
|
|
||||||
there is an inherent amount of complexity in configuring and deploying web
|
|
||||||
applications. If you find yourself a little lost, please don't hesitate to
|
|
||||||
[reach out for help](/help.md)
|
|
||||||
|
|
||||||
## Deployment Scenarios
|
|
||||||
|
|
||||||
### Embedded Viewer (deprecated)
|
|
||||||
`OHIF-v3` has deprecated deploying the viewer as an embedded viewer the number of underlying
|
|
||||||
libraries that run web workers are increasing for OHIF. An example of these libraries is
|
|
||||||
OHIF's 3D rendering functionality that is provided by `vtk-js`.
|
|
||||||
|
|
||||||
### Stand-alone Viewer
|
|
||||||
|
|
||||||
Deploying the OHIF Viewer as a stand-alone web application provides many
|
|
||||||
benefits, but comes at the cost of time and complexity. Some benefits include:
|
|
||||||
|
|
||||||
_Today:_
|
|
||||||
|
|
||||||
- Leverage [extensions](/extensions/index.md) and [modes](/modes/index.md) to drop-in powerful new features
|
|
||||||
- Add routes and customize the viewer's workflow
|
|
||||||
- Finer control over styling and whitelabeling
|
|
||||||
|
|
||||||
_In the future:_
|
|
||||||
|
|
||||||
- The ability to package the viewer for [App Store distribution][app-store]
|
|
||||||
- Leverage `service-workers` for offline support and speed benefits from caching
|
|
||||||
|
|
||||||
#### Hosted Static Assets
|
|
||||||
|
|
||||||
At the end of the day, a production OHIF Viewer instance is a collection of
|
|
||||||
HTML, CSS, JS, Font Files, and Images. We "build" those files from our
|
|
||||||
`source code` with configuration specific to our project. We then make those
|
|
||||||
files publicly accessible by hosting them on a Web Server.
|
|
||||||
|
|
||||||
If you have not deployed a web application before, this may be a good time to
|
|
||||||
[reach out for help](/help.md), as these steps assume prior web development and
|
|
||||||
deployment experience.
|
|
||||||
|
|
||||||
##### Part 1 - Build Production Assets
|
|
||||||
|
|
||||||
"Building", or creating, the files you will need is the same regardless of the
|
|
||||||
web host you choose. You can find detailed instructions on how to configure and
|
|
||||||
build the OHIF Viewer in our
|
|
||||||
["Build for Production" guide](./recipes/build-for-production.md).
|
|
||||||
|
|
||||||
##### Part 2 - Host Your App
|
|
||||||
|
|
||||||
There are a lot of [benefits to hosting static assets][host-static-assets] over
|
|
||||||
dynamic content. You can find instructions on how to host your build's output
|
|
||||||
via one of these guides:
|
|
||||||
|
|
||||||
_Drag-n-drop_
|
|
||||||
|
|
||||||
- [Netlify: Drop](/deployment/recipes/static-assets.md#netlify-drop)
|
|
||||||
|
|
||||||
_Easy_
|
|
||||||
|
|
||||||
- [Surge.sh](/deployment/recipes/static-assets.md#surgesh)
|
|
||||||
- [GitHub Pages](/deployment/recipes/static-assets.md#github-pages)
|
|
||||||
|
|
||||||
_Advanced_
|
|
||||||
|
|
||||||
- [AWS S3 + Cloudfront](/deployment/recipes/static-assets.md#aws-s3--cloudfront)
|
|
||||||
- [GCP + Cloudflare](/deployment/recipes/static-assets.md#gcp--cloudflare)
|
|
||||||
- [Azure](/deployment/recipes/static-assets.md#azure)
|
|
||||||
|
|
||||||
## Data
|
|
||||||
|
|
||||||
The OHIF Viewer is able to connect to any data source that implements the [DICOM
|
|
||||||
Web Standard][dicom-web-standard]. [DICOM Web][dicom-web] refers to RESTful
|
|
||||||
DICOM Services -- a recently standardized set of guidelines for exchanging
|
|
||||||
medical images and imaging metadata over the internet. Not all archives fully
|
|
||||||
support it yet, but it is gaining wider adoption.
|
|
||||||
|
|
||||||
### Configure Connection
|
|
||||||
|
|
||||||
If you have an existing archive and intend to host the OHIF Viewer at the same
|
|
||||||
domain name as your archive, then connecting the two is as simple as following
|
|
||||||
the steps layed out in our
|
|
||||||
[Configuration Essentials Guide](./../configuring/index.md).
|
|
||||||
|
|
||||||
#### What if I don't have an imaging archive?
|
|
||||||
|
|
||||||
We provide some guidance on configuring a local image archive in our
|
|
||||||
[Data Source Essentials](./../configuring/index.md#set-up-a-local-DICOM-server) guide. Hosting an
|
|
||||||
archive remotely is a little trickier. You can check out some of our
|
|
||||||
[advanced recipes](#recipes) for modeled setups that may work for you.
|
|
||||||
|
|
||||||
#### What if I intend to host the OHIF Viewer at a different domain?
|
|
||||||
|
|
||||||
There are two important steps to making sure this setup works:
|
|
||||||
|
|
||||||
1. Your Image Archive needs to be exposed, in some way, to the open web. This
|
|
||||||
can be directly, or through a `reverse proxy`, but the Viewer needs _some
|
|
||||||
way_ to request it's data.
|
|
||||||
2. \* Your Image Archive needs to have appropriate CORS (Cross-Origin Resource
|
|
||||||
Sharing) Headers
|
|
||||||
|
|
||||||
> \* Cross-Origin Resource Sharing (CORS) is a mechanism that uses additional
|
|
||||||
> HTTP headers to tell a browser to let a web application running at one origin
|
|
||||||
> (domain) have permission to access selected resources from a server at a
|
|
||||||
> different origin. - [MDN Web Docs: Web - Http - CORS][cors]
|
|
||||||
|
|
||||||
Most image archives do not provide either of these features "out of the box".
|
|
||||||
It's common to use IIS, Nginx, or Apache to route incoming requests and append
|
|
||||||
appropriate headers. You can find an example of this setup in our
|
|
||||||
[Nginx + Image Archive Deployment Recipe](./recipes/nginx--image-archive.md).
|
|
||||||
|
|
||||||
#### What if my archive doesn't support DicomWeb?
|
|
||||||
|
|
||||||
It's possible to supply all Study data via JSON format, in the event you do not have a DicomWeb endpoint.
|
|
||||||
You can host all of the relevant files on any web accessible server (Amazon S3, Azure Blob Storage, Local file server etc.)
|
|
||||||
|
|
||||||
This JSON is supplied via the '?url=' query parameter.
|
|
||||||
It should reference an endpoint that returns **application/json** formatted text.
|
|
||||||
|
|
||||||
If you do not have an API, you can simply return a text file containing the JSON from any web server.
|
|
||||||
|
|
||||||
|
|
||||||
You tell the OHIF viewer to use JSON by using the `dicomjson` datasource and appending `'?url='` query to your mode's route:
|
|
||||||
|
|
||||||
eg. `https://my-test-ohif-server/myMode/dicomjson?url=https://my-json-server/study-uid.json`
|
|
||||||
|
|
||||||
|
|
||||||
The returned JSON object must contain a single root object with a 'studies' array.
|
|
||||||
|
|
||||||
You can read more about using different data sources for mode's routes [here](../modes/routes.md#route-path)
|
|
||||||
|
|
||||||
*Sample JSON format:*
|
|
||||||
```JSON
|
|
||||||
{
|
|
||||||
"studies": [
|
|
||||||
{
|
|
||||||
"StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78",
|
|
||||||
"StudyDescription": "BRAIN SELLA",
|
|
||||||
"StudyDate": "20010108",
|
|
||||||
"StudyTime": "120022",
|
|
||||||
"PatientName": "MISTER^MR",
|
|
||||||
"PatientId": "832040",
|
|
||||||
"series": [
|
|
||||||
{
|
|
||||||
"SeriesDescription": "SAG T-1",
|
|
||||||
"SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.121",
|
|
||||||
"SeriesNumber": 2,
|
|
||||||
"SeriesDate": "20010108",
|
|
||||||
"SeriesTime": "120318",
|
|
||||||
"Modality": "MR",
|
|
||||||
"instances": [
|
|
||||||
{
|
|
||||||
"metadata": {
|
|
||||||
"Columns": 512,
|
|
||||||
"Rows": 512,
|
|
||||||
"InstanceNumber": 3,
|
|
||||||
"AcquisitionNumber": 0,
|
|
||||||
"PhotometricInterpretation": "MONOCHROME2",
|
|
||||||
"BitsAllocated": 16,
|
|
||||||
"BitsStored": 16,
|
|
||||||
"PixelRepresentation": 1,
|
|
||||||
"SamplesPerPixel": 1,
|
|
||||||
"PixelSpacing": [0.390625, 0.390625],
|
|
||||||
"HighBit": 15,
|
|
||||||
"ImageOrientationPatient": [0,1,0,0,0,-1],
|
|
||||||
"ImagePositionPatient": [11.600000,-92.500000, 98.099998],
|
|
||||||
"FrameOfReferenceUID": "1.2.840.113619.2.5.1762583153.223134.978956938.470",
|
|
||||||
"ImageType": ["ORIGINAL","PRIMARY","OTHER"],
|
|
||||||
"Modality": "MR",
|
|
||||||
"SOPInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.124",
|
|
||||||
"SeriesInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.121",
|
|
||||||
"StudyInstanceUID": "1.2.840.113619.2.5.1762583153.215519.978957063.78"
|
|
||||||
},
|
|
||||||
"url": "dicomweb://s3.amazonaws.com/lury/MRStudy/1.2.840.113619.2.5.1762583153.215519.978957063.124.dcm"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
More info on this JSON format can be found here [Issue #1500](https://github.com/OHIF/Viewers/issues/1500)
|
|
||||||
|
|
||||||
|
|
||||||
**Implementation Notes:**
|
|
||||||
|
|
||||||
<!-- 1. When hosting the viewer, you will also need to host a /viewer route on the server - or the browser may not be able to find the route. -->
|
|
||||||
1. For each instance url (dicom object) in the returned JSON, you must prefix the `url` with `dicomjson:` in order for the cornerstone image loader to retrieve it correctly.
|
|
||||||
eg. `https://image-server/my-image.dcm` ---> `dicomjson:https://image-server/my-image.dcm`
|
|
||||||
2. The JSON format above is compatible with >= v3.7.8 of the application in `V2` version. Older versions of the viewer used a different JSON format. As of 20/04/20 the public [https://viewer.ohif.org/] is a pre 3.0 version that does not support this format yet.
|
|
||||||
3. The JSON format is case-sensitive. Please ensure you have matched casing with the naturalised Dicom format referenced in [Issue #1500](https://github.com/OHIF/Viewers/issues/1500).
|
|
||||||
|
|
||||||
*CORS Issues (Cross-Origin Resource Sharing)*
|
|
||||||
|
|
||||||
If you host a JSON API or Images on a different domain from the the app itself, you will likely have CORS issues. This will also happen when testing from Localhost and reaching out to remote servers.
|
|
||||||
Even if the domain is the same, different ports, subdomains or protocols (https vs http) will also cause CORS errors.
|
|
||||||
You will to need add a configuration on each server hosting these assets to allow your App server origin.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
Lets assume your application is hosted on `https://my-ohif-server.com`.
|
|
||||||
|
|
||||||
Your JSON API is hosted on `https://my-json-api.aws.com`
|
|
||||||
|
|
||||||
And your images are stored on Amazon S3 at `https://my-s3-bucket.aws.com`
|
|
||||||
|
|
||||||
When you first start your application, browsing to `https://my-ohif-server.com/myMode/dicomjson?url=https://my-json-api.aws.com/api/my-json-study-info.json`, you will likely get a CORS error in the browser console as it tries to connect to `https://my-json-api.aws.com`.
|
|
||||||
|
|
||||||
Adding a setting on the JSON server to allow the CORS origin = `https://my-ohif-server.com` should solve this.
|
|
||||||
|
|
||||||
Next, you will likely get a similar CORS error, as the browser tries to go to `https://my-s3-bucket.aws.com`.
|
|
||||||
You will need to go to the S3 bucket configuration, and add a CORS setting to allow origin = `https://my-ohif-server.com`.
|
|
||||||
|
|
||||||
Essentially, whenever the application connects to a remote resource, you will need to add the applications url to the allowed CORS Origins on that resource. Adding an origin similar to https://localhost:3000 will also allow for local testing.
|
|
||||||
|
|
||||||
|
|
||||||
### Securing Your Data
|
|
||||||
Coming soon
|
|
||||||
|
|
||||||
<!--
|
|
||||||
> Feeling lost? Securing your data is important, and it can be hard to tell if
|
|
||||||
> you've gotten it right. Don't hesitate to work with professional auditors, or
|
|
||||||
> [enlist help from experts](./../help.md).
|
|
||||||
|
|
||||||
The OHIF Viewer can be configured to work with authorization servers that
|
|
||||||
support one or more of the OpenID-Connect authorization flows. The Viewer finds
|
|
||||||
it's OpenID-Connect settings on the `oidc` configuration key. You can set these
|
|
||||||
values following the instructions laid out in the
|
|
||||||
[Configuration Essentials Guide](./../configuring/index.md).
|
|
||||||
|
|
||||||
_Example OpenID-Connect Settings:_
|
|
||||||
|
|
||||||
```js
|
|
||||||
window.config = {
|
|
||||||
...
|
|
||||||
oidc: [
|
|
||||||
{
|
|
||||||
// ~ REQUIRED
|
|
||||||
// Authorization Server URL
|
|
||||||
authority: 'http://127.0.0.1/auth/realms/ohif',
|
|
||||||
client_id: 'ohif-viewer',
|
|
||||||
redirect_uri: 'http://127.0.0.1/callback', // `OHIFStandaloneViewer.js`
|
|
||||||
response_type: 'code', // "Authorization Code Flow"
|
|
||||||
scope: 'openid', // email profile openid
|
|
||||||
// ~ OPTIONAL
|
|
||||||
post_logout_redirect_uri: '/logout-redirect.html',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
You can find an example of this setup in our
|
|
||||||
[User Account Control Deployment Recipe](./recipes/user-account-control.md).
|
|
||||||
|
|
||||||
#### Choosing a Flow for the Viewer
|
|
||||||
|
|
||||||
In general, we recommend using the "Authorization Code Flow" ( [see
|
|
||||||
`response_type=code` here][code-flows]); however, the "Implicit Flow" ( [see
|
|
||||||
`response_type=token` here][code-flows]) can work if additonal precautions are
|
|
||||||
taken. If the flow you've chosen produces a JWT Token, it's validity can be used
|
|
||||||
to secure access to your Image Archive as well.
|
|
||||||
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Recipes
|
|
||||||
|
|
||||||
We've included a few recipes for common deployment scenarios. There are many,
|
|
||||||
many possible configurations, so please don't feel limited to these setups.
|
|
||||||
Please feel free to suggest or contribute your own recipes.
|
|
||||||
|
|
||||||
|
|
||||||
- [Build for Production](./recipes/build-for-production.md)
|
|
||||||
- [Static](./recipes/static-assets.md)
|
|
||||||
- [Nginx + Image Archive](./recipes/nginx--image-archive.md)
|
|
||||||
- [User Account Control](./recipes/user-account-control.md)
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Links
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
|
||||||
[viewer-npm]: https://www.npmjs.com/package/@ohif/viewer
|
|
||||||
[pwa-url]: https://developers.google.com/web/progressive-web-apps/
|
|
||||||
[static-assets-url]: https://www.maxcdn.com/one/visual-glossary/static-content/
|
|
||||||
[app-store]: https://medium.freecodecamp.org/i-built-a-pwa-and-published-it-in-3-app-stores-heres-what-i-learned-7cb3f56daf9b
|
|
||||||
[dicom-web-standard]: https://www.dicomstandard.org/dicomweb/
|
|
||||||
[dicom-web]: https://en.wikipedia.org/wiki/DICOMweb
|
|
||||||
[host-static-assets]: https://www.netlify.com/blog/2016/05/18/9-reasons-your-site-should-be-static/
|
|
||||||
[cors]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
|
|
||||||
[code-flows]: https://medium.com/@darutk/diagrams-of-all-the-openid-connect-flows-6968e3990660
|
|
||||||
[code-sandbox]: https://codesandbox.io/s/viewer-script-tag-tprch
|
|
||||||
<!-- prettier-ignore-end -->
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
# Build for Production
|
|
||||||
|
|
||||||
> If you've already followed the
|
|
||||||
> ["Getting Started" Guide](/development/getting-started.md), you can skip ahead
|
|
||||||
> to [Configuration](#configuration)
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
### Build Machine Requirements
|
|
||||||
|
|
||||||
- [Node.js & NPM](https://nodejs.org/en/download/)
|
|
||||||
- [Yarn](https://yarnpkg.com/lang/en/docs/install/)
|
|
||||||
- [Git](https://www.atlassian.com/git/tutorials/install-git)
|
|
||||||
|
|
||||||
### Getting the Code
|
|
||||||
|
|
||||||
_With Git:_
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Clone the remote repository to your local machine
|
|
||||||
git clone https://github.com/OHIF/Viewers.git
|
|
||||||
```
|
|
||||||
|
|
||||||
More on: _[`git clone`](https://git-scm.com/docs/git-clone),
|
|
||||||
[`git checkout`](https://git-scm.com/docs/git-checkout)_
|
|
||||||
|
|
||||||
_From .zip:_
|
|
||||||
|
|
||||||
[OHIF/Viewers: react.zip](https://github.com/OHIF/Viewers/archive/master.zip)
|
|
||||||
|
|
||||||
### Restore Dependencies & Build
|
|
||||||
|
|
||||||
Open your terminal, and navigate to the directory containing the source files.
|
|
||||||
Next run these commands:
|
|
||||||
|
|
||||||
```js
|
|
||||||
// If you haven't already, enable yarn workspaces
|
|
||||||
yarn config set workspaces-experimental true
|
|
||||||
|
|
||||||
// Restore dependencies
|
|
||||||
yarn install
|
|
||||||
|
|
||||||
// Build source code for production
|
|
||||||
yarn run build
|
|
||||||
```
|
|
||||||
|
|
||||||
If everything worked as expected, you should have a new `dist/` directory in the
|
|
||||||
project's folder. It should roughly resemble the following:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
<root>platform/viewer/dist/
|
|
||||||
├── app-config.js
|
|
||||||
├── app.bundle.js
|
|
||||||
├── app.css
|
|
||||||
├── index.html
|
|
||||||
├── manifest.json
|
|
||||||
├── service-worker.js
|
|
||||||
└── ...
|
|
||||||
```
|
|
||||||
|
|
||||||
By default, the build output will connect to OHIF's publicly accessible PACS. If
|
|
||||||
this is your first time setting up the OHIF Viewer, it is recommended that you
|
|
||||||
test with these default settings. After testing, you can find instructions on
|
|
||||||
how to configure the project for your own imaging archive below.
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
|
|
||||||
The configuration for our viewer is in the `<root>platform/viewer/public/config`
|
|
||||||
directory. Our build process knows which configuration file to use based on the
|
|
||||||
`APP_CONFIG` environment variable. By default, its value is
|
|
||||||
[`config/default.js`][default-config]. The majority of the viewer's features,
|
|
||||||
and registered extension's features, are configured using this file.
|
|
||||||
|
|
||||||
The easiest way to apply your own configuration is to modify the `default.js`
|
|
||||||
file. For more advanced cofiguration options, check out our
|
|
||||||
[configuration essentials guide](/configuring/index.md).
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
### Deploying Build Output
|
|
||||||
|
|
||||||
_Drag-n-drop_
|
|
||||||
|
|
||||||
- [Netlify: Drop](/deployment/recipes/static-assets.md#netlify-drop)
|
|
||||||
|
|
||||||
_Easy_
|
|
||||||
|
|
||||||
- [Surge.sh](/deployment/recipes/static-assets.md#surgesh)
|
|
||||||
- [GitHub Pages](/deployment/recipes/static-assets.md#github-pages)
|
|
||||||
|
|
||||||
_Advanced_
|
|
||||||
|
|
||||||
- [AWS S3 + Cloudfront](/deployment/recipes/static-assets.md#aws-s3--cloudfront)
|
|
||||||
- [GCP + Cloudflare](/deployment/recipes/static-assets.md#gcp--cloudflare)
|
|
||||||
- [Azure](/deployment/recipes/static-assets.md#azure)
|
|
||||||
|
|
||||||
### Testing Build Output Locally
|
|
||||||
|
|
||||||
A quick way to test your build output locally is to spin up a small webserver.
|
|
||||||
You can do this by running the following commands in the `dist/` output
|
|
||||||
directory:
|
|
||||||
|
|
||||||
```js
|
|
||||||
// Install http-server as a globally available package
|
|
||||||
yarn global add http-server
|
|
||||||
|
|
||||||
// Change the directory to the platform/viewer
|
|
||||||
|
|
||||||
// Serve the files in our current directory
|
|
||||||
// Accessible at: `http://localhost:8080`
|
|
||||||
npx http-server ./dist
|
|
||||||
```
|
|
||||||
|
|
||||||
<div style="padding:56.25% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/551957266?badge=0&autopause=0&player_id=0&app_id=58479" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen style="position:absolute;top:0;left:0;width:100%;height:100%;" title="HTTP Server Deploy"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
|
|
||||||
|
|
||||||
### Automating Builds and Deployments
|
|
||||||
|
|
||||||
If you found setting up your environment and running all of these steps to be a
|
|
||||||
bit tedious, then you are in good company. Thankfully, there are a large number
|
|
||||||
of tools available to assist with automating tasks like building and deploying
|
|
||||||
web application. For a starting point, check out this repository's own use of:
|
|
||||||
|
|
||||||
- [CircleCI][circleci]: [config.yaml][circleci-config]
|
|
||||||
- [Netlify][netlify]: [netlify.toml][netlify.toml] |
|
|
||||||
[build-deploy-preview.sh][build-deploy-preview.sh]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
|
||||||
[circleci]: https://circleci.com/gh/OHIF/Viewers
|
|
||||||
[circleci-config]: https://github.com/OHIF/Viewers/blob/master/.circleci/config.yml
|
|
||||||
[netlify]: https://app.netlify.com/sites/ohif/deploys
|
|
||||||
[netlify.toml]: https://github.com/OHIF/Viewers/blob/master/netlify.toml
|
|
||||||
[build-deploy-preview.sh]: https://github.com/OHIF/Viewers/blob/master/.netlify/build-deploy-preview.sh
|
|
||||||
<!-- prettier-ignore-end -->
|
|
||||||
@@ -1,264 +0,0 @@
|
|||||||
# Nginx + Image Archive
|
|
||||||
|
|
||||||
> DISCLAIMER! We make no claims or guarantees of this approach's security. If in
|
|
||||||
> doubt, enlist the help of an expert and conduct proper audits.
|
|
||||||
|
|
||||||
At a certain point, you may want others to have access to your instance of the
|
|
||||||
OHIF Viewer and its medical imaging data. This post covers one of many potential
|
|
||||||
setups that accomplish that. Please note, noticably absent is user account
|
|
||||||
control.
|
|
||||||
|
|
||||||
Do not use this recipe to host sensitive medical data on the open web. Depending
|
|
||||||
on your company's policies, this may be an appropriate setup on an internal
|
|
||||||
network when protected with a server's basic authentication. For a more robust
|
|
||||||
setup, check out our [user account control recipe](./user-account-control.md)
|
|
||||||
that builds on the lessons learned here.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Our two biggest hurdles when hosting our image archive and web client are:
|
|
||||||
|
|
||||||
- Risks related to exposing our PACS to the netowrk
|
|
||||||
- Cross-Origin Resource Sharing (CORS) requests
|
|
||||||
|
|
||||||
### Handling Web Requests
|
|
||||||
|
|
||||||
We mittigate our first issue by allowing [Nginx][nginx] to handle incoming web
|
|
||||||
requests. Nginx is open source software for web serving, reverse proxying,
|
|
||||||
caching, and more. It's designed for maximum performance and stability --
|
|
||||||
allowing us to more reliably serve content than Orthanc's built-in server can.
|
|
||||||
|
|
||||||
More specifically, we accomplish this by using a
|
|
||||||
[`reverse proxy`](https://en.wikipedia.org/wiki/Reverse_proxy) to retrieve
|
|
||||||
resources from our image archive (Orthanc), and when accessing its web admin.
|
|
||||||
|
|
||||||
> A reverse proxy is a type of proxy server that retrieves resources on behalf
|
|
||||||
> of a client from one or more servers. These resources are then returned to the
|
|
||||||
> client, appearing as if they originated from the proxy server itself.
|
|
||||||
|
|
||||||
### CORS Issues
|
|
||||||
|
|
||||||
Cross-Origin Resource Sharing (CORS) is a mechanism that uses HTTP headers to
|
|
||||||
tell a browser which web applications have permission to access selected
|
|
||||||
resources from a server at a different origin (domain, protocol, port). IE. By
|
|
||||||
default, a Web App located at `http://my-website.com` can't access resources
|
|
||||||
hosted at `http://not-my-website.com`
|
|
||||||
|
|
||||||
We can solve this one of two ways:
|
|
||||||
|
|
||||||
1. Have our Image Archive located at the same domain as our Web App
|
|
||||||
2. Add appropriate `Access-Control-Allow-*` HTTP headers
|
|
||||||
|
|
||||||
**This solution uses the first approach.**
|
|
||||||
|
|
||||||
You can read more about CORS in this Medium article: [Understanding
|
|
||||||
CORS][understanding-cors]
|
|
||||||
|
|
||||||
### Diagram
|
|
||||||
|
|
||||||
This setup allows us to create a setup similar to the one pictured below:
|
|
||||||
|
|
||||||
{% include "./../_nginx-image-archive-diagram.md" %}
|
|
||||||
|
|
||||||
- All web requests are routed through `nginx` on our `OpenResty` image
|
|
||||||
- `/pacs` is a reverse proxy for `orthanc`'s `DICOM Web` endpoints
|
|
||||||
- `/pacs-admin` is a reverse proxy for `orthanc`'s Web Admin
|
|
||||||
- All static resources for OHIF Viewer are served up by `nginx` when a matching
|
|
||||||
route for that resource is requested
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
### Requirements
|
|
||||||
|
|
||||||
- Docker
|
|
||||||
- [Docker for Mac](https://docs.docker.com/docker-for-mac/)
|
|
||||||
- [Docker for Windows](https://docs.docker.com/docker-for-windows/)
|
|
||||||
|
|
||||||
_Not sure if you have `docker` installed already? Try running `docker --version`
|
|
||||||
in command prompt or terminal_
|
|
||||||
|
|
||||||
### Setup
|
|
||||||
|
|
||||||
- Navigate to `viewer` folder inside `platform`
|
|
||||||
- then: `cd .recipes/OpenResty-Orthanc`
|
|
||||||
- run: `docker-compose up --build`
|
|
||||||
- Navigate to `127.0.0.1` for the viewer
|
|
||||||
- Navigate to `127.0.0.1/pacs-admin` for uploading studies
|
|
||||||
|
|
||||||
|
|
||||||
You can see the overview of the mentioned steps:
|
|
||||||
|
|
||||||
|
|
||||||
<div style="padding:56.25% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/554726410?badge=0&autopause=0&player_id=0&app_id=58479" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen style="position:absolute;top:0;left:0;width:100%;height:100%;" title="NginX Deployment"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
|
|
||||||
|
|
||||||
<!-- ### Setup
|
|
||||||
|
|
||||||
_Spin Things Up_
|
|
||||||
|
|
||||||
- Navigate to `<project-root>/docker/OpenResty-Orthanc` in your shell
|
|
||||||
- Run `docker-compose up`
|
|
||||||
|
|
||||||
_Upload Your First Study_
|
|
||||||
|
|
||||||
- Navigate to `http://127.0.0.1/pacs-admin`
|
|
||||||
- From the top right, select "Upload"
|
|
||||||
- Click "Select files to upload..." (DICOM)
|
|
||||||
- Click "Start the upload"
|
|
||||||
- Navigate back to `http://127.0.0.1/` to view your studies in the Study List -->
|
|
||||||
|
|
||||||
### Troubleshooting
|
|
||||||
|
|
||||||
_Exit code 137_
|
|
||||||
|
|
||||||
This means Docker ran out of memory. Open Docker Desktop, go to the `advanced`
|
|
||||||
tab, and increase the amount of Memory available.
|
|
||||||
|
|
||||||
_Cannot create container for service X_
|
|
||||||
|
|
||||||
Use this one with caution: `docker system prune`
|
|
||||||
|
|
||||||
_X is already running_
|
|
||||||
|
|
||||||
Stop running all containers:
|
|
||||||
|
|
||||||
- Win: `docker ps -a -q | ForEach { docker stop $_ }`
|
|
||||||
- Linux: `docker stop $(docker ps -a -q)`
|
|
||||||
|
|
||||||
|
|
||||||
_Traceback (most recent call last):_
|
|
||||||
_File "urllib3/connectionpool.py", line 670, in urlopen_
|
|
||||||
_...._
|
|
||||||
|
|
||||||
Are you sure your docker is running? see explanation [here](https://github.com/docker/compose/issues/7896)
|
|
||||||
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
|
|
||||||
After verifying that everything runs with default configuration values, you will
|
|
||||||
likely want to update:
|
|
||||||
|
|
||||||
- The domain: `http://127.0.0.1`
|
|
||||||
|
|
||||||
#### OHIF Viewer
|
|
||||||
|
|
||||||
The OHIF Viewer's configuration is imported from a static `.js` file. The
|
|
||||||
configuration we use is set to a specific file when we build the viewer, and
|
|
||||||
determined by the env variable: `APP_CONFIG`. You can see where we set its value
|
|
||||||
in the `dockerfile` for this solution:
|
|
||||||
|
|
||||||
`ENV APP_CONFIG=config/docker_openresty-orthanc.js`
|
|
||||||
|
|
||||||
You can find the configuration we're using here:
|
|
||||||
`/public/config/docker_openresty-orthanc.js`
|
|
||||||
|
|
||||||
To rebuild the `webapp` image created by our `dockerfile` after updating the
|
|
||||||
Viewer's configuration, you can run:
|
|
||||||
|
|
||||||
- `docker-compose build` OR
|
|
||||||
- `docker-compose up --build`
|
|
||||||
|
|
||||||
#### Other
|
|
||||||
|
|
||||||
All other files are found in: `/docker/OpenResty-Orthanc/`
|
|
||||||
|
|
||||||
| Service | Configuration | Docs |
|
|
||||||
| ----------------- | --------------------------------- | ------------------------------------------- |
|
|
||||||
| OHIF Viewer | [dockerfile][dockerfile] | You're reading them now! |
|
|
||||||
| OpenResty (Nginx) | [`/nginx.conf`][config-nginx] | [lua-resty-openidc][lua-resty-openidc-docs] |
|
|
||||||
| Orthanc | [`/orthanc.json`][config-orthanc] | [Here][orthanc-docs] |
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
### Deploying to Production
|
|
||||||
|
|
||||||
While these configuration and docker-compose files model an environment suitable
|
|
||||||
for production, they are not easy to deploy "as is". You can either:
|
|
||||||
|
|
||||||
- Manually recreate this environment and deploy built application files **OR**
|
|
||||||
- Deploy to a cloud kubernetes provider like
|
|
||||||
[Digital Ocean](https://www.digitalocean.com/products/kubernetes/) **OR**
|
|
||||||
- [See a full list of cloud providers here](https://landscape.cncf.io/category=cloud&format=card-mode&grouping=category)
|
|
||||||
- Find and follow your preferred provider's guide on setting up
|
|
||||||
[swarms and stacks](https://docs.docker.com/get-started/)
|
|
||||||
|
|
||||||
### Adding SSL
|
|
||||||
|
|
||||||
Adding SSL registration and renewal for your domain with Let's Encrypt that
|
|
||||||
terminates at Nginx is an incredibly important step toward securing your data.
|
|
||||||
Here are some resources, specific to this setup, that may be helpful:
|
|
||||||
|
|
||||||
- [lua-resty-auto-ssl](https://github.com/GUI/lua-resty-auto-ssl)
|
|
||||||
- [Let's Encrypt + Nginx](https://www.nginx.com/blog/using-free-ssltls-certificates-from-lets-encrypt-with-nginx/)
|
|
||||||
|
|
||||||
While we terminate SSL at Nginx, it may be worth using self signed certificates
|
|
||||||
for communication between services.
|
|
||||||
|
|
||||||
- [SSL Termination for TCP Upstream Servers](https://docs.nginx.com/nginx/admin-guide/security-controls/terminating-ssl-tcp/)
|
|
||||||
|
|
||||||
### Use PostgresSQL w/ Orthanc
|
|
||||||
|
|
||||||
Orthanc can handle a large amount of data and requests, but if you find that
|
|
||||||
requests start to slow as you add more and more studies, you may want to
|
|
||||||
configure your Orthanc instance to use PostgresSQL. Instructions on how to do
|
|
||||||
that can be found in the
|
|
||||||
[`Orthanc Server Book`](http://book.orthanc-server.com/users/docker.html), under
|
|
||||||
"PostgreSQL and Orthanc inside Docker"
|
|
||||||
|
|
||||||
### Improving This Guide
|
|
||||||
|
|
||||||
Here are some improvements this guide would benefit from, and that we would be
|
|
||||||
more than happy to accept Pull Requests for:
|
|
||||||
|
|
||||||
- SSL Support
|
|
||||||
- Complete configuration with `.env` file (or something similar)
|
|
||||||
- Any security issues
|
|
||||||
- One-click deploy to a cloud provider
|
|
||||||
|
|
||||||
## Resources
|
|
||||||
|
|
||||||
### Misc. Helpful Commands
|
|
||||||
|
|
||||||
_Check if `nginx.conf` is valid:_
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run --rm -t -a stdout --name my-openresty -v $PWD/config/:/usr/local/openresty/nginx/conf/:ro openresty/openresty:alpine-fat openresty -c /usr/local/openresty/nginx/conf/nginx.conf -t
|
|
||||||
```
|
|
||||||
|
|
||||||
_Interact w/ running container:_
|
|
||||||
|
|
||||||
`docker exec -it CONTAINER_NAME bash`
|
|
||||||
|
|
||||||
_List running containers:_
|
|
||||||
|
|
||||||
`docker ps`
|
|
||||||
|
|
||||||
### Referenced Articles
|
|
||||||
|
|
||||||
For more documentation on the software we've chosen to use, you may find the
|
|
||||||
following resources helpful:
|
|
||||||
|
|
||||||
- [Orthanc for Docker](http://book.orthanc-server.com/users/docker.html)
|
|
||||||
- [OpenResty Guide](http://www.staticshin.com/programming/definitely-an-open-resty-guide/)
|
|
||||||
- [Lua Ngx API](https://openresty-reference.readthedocs.io/en/latest/Lua_Nginx_API/)
|
|
||||||
|
|
||||||
For a different take on this setup, check out the repositories our community
|
|
||||||
members put together:
|
|
||||||
|
|
||||||
- [mjstealey/ohif-orthanc-dimse-docker](https://github.com/mjstealey/ohif-orthanc-dimse-docker)
|
|
||||||
- [trypag/ohif-orthanc-postgres-docker](https://github.com/trypag/ohif-orthanc-postgres-docker)
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Links
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
|
||||||
<!-- DOCS -->
|
|
||||||
[nginx]: https://www.nginx.com/resources/glossary/nginx/
|
|
||||||
[understanding-cors]: https://medium.com/@baphemot/understanding-cors-18ad6b478e2b
|
|
||||||
[orthanc-docs]: http://book.orthanc-server.com/users/configuration.html#configuration
|
|
||||||
[lua-resty-openidc-docs]: https://github.com/zmartzone/lua-resty-openidc
|
|
||||||
<!-- SRC -->
|
|
||||||
[dockerfile]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc/dockerfile
|
|
||||||
[config-nginx]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc/config/nginx.conf
|
|
||||||
[config-orthanc]: https://github.com/OHIF/Viewers/blob/master/platform/viewer/.recipes/OpenResty-Orthanc/config/orthanc.json
|
|
||||||
<!-- prettier-ignore-end -->
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
# Deploy Static Assets
|
|
||||||
|
|
||||||
> WARNING! All of these solutions stand-up a publicly accessible web viewer. Do
|
|
||||||
> not hook your hosted viewer up to a sensitive source of data without
|
|
||||||
> implementing authentication.
|
|
||||||
|
|
||||||
There are a lot of options for deploying static assets. Some services, like
|
|
||||||
`netlify` and `surge.sh`, specialize in static websites. You'll notice that
|
|
||||||
deploying with them requires much less time and effort, but comes at the cost of
|
|
||||||
less product offerings.
|
|
||||||
|
|
||||||
While not required, it can simplify things to host your Web Viewer alongside
|
|
||||||
your image archive. Services with more robust product offerings, like
|
|
||||||
`Google Cloud`, `Microsoft's Azure`, and `Amazon Web Services (AWS)`, are able
|
|
||||||
to accomodate this setup.
|
|
||||||
|
|
||||||
_Drag-n-drop_
|
|
||||||
|
|
||||||
- [Netlify: Drop](#netlify-drop)
|
|
||||||
|
|
||||||
_Easy_
|
|
||||||
|
|
||||||
- [Surge.sh](#surgesh)
|
|
||||||
- [GitHub Pages](#github-pages)
|
|
||||||
|
|
||||||
_Advanced_
|
|
||||||
|
|
||||||
- [Deploy Static Assets](#deploy-static-assets)
|
|
||||||
- [Drag-n-drop](#drag-n-drop)
|
|
||||||
- [Netlify: Drop](#netlify-drop)
|
|
||||||
- [Easy](#easy)
|
|
||||||
- [Surge.sh](#surgesh)
|
|
||||||
- [GitHub Pages](#github-pages)
|
|
||||||
- [Advanced](#advanced)
|
|
||||||
- [AWS S3 + Cloudfront](#aws-s3--cloudfront)
|
|
||||||
- [GCP + Cloudflare](#gcp--cloudflare)
|
|
||||||
- [Azure](#azure)
|
|
||||||
|
|
||||||
## Drag-n-drop
|
|
||||||
|
|
||||||
### Netlify: Drop
|
|
||||||
|
|
||||||
<div style="padding:56.25% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/551957463?badge=0&autopause=0&player_id=0&app_id=58479" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen style="position:absolute;top:0;left:0;width:100%;height:100%;" title="Netlify Drop"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
|
|
||||||
|
|
||||||
|
|
||||||
_GIF demonstrating deployment with Netlify Drop_
|
|
||||||
|
|
||||||
1. https://app.netlify.com/drop
|
|
||||||
2. Drag your `build/` folder on to the drop target
|
|
||||||
3. ...
|
|
||||||
4. _annnd you're done_
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
|
|
||||||
- Custom domains & HTTPS
|
|
||||||
- Instant Git integration
|
|
||||||
- Continuous deployment
|
|
||||||
- Deploy previews
|
|
||||||
- Access to add-ons
|
|
||||||
|
|
||||||
(Non-free tiers include identity, FaaS, Forms, etc.)
|
|
||||||
|
|
||||||
Learn more about [Netlify on their website](https://www.netlify.com/)
|
|
||||||
|
|
||||||
## Easy
|
|
||||||
|
|
||||||
### Surge.sh
|
|
||||||
|
|
||||||
> Static web publishing for Front-End Developers. Simple, single-command web
|
|
||||||
> publishing. Publish HTML, CSS, and JS for free, without leaving the command
|
|
||||||
> line.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
_GIF demonstrating deployment with surge_
|
|
||||||
|
|
||||||
```shell
|
|
||||||
# Add surge command
|
|
||||||
yarn global add surge
|
|
||||||
|
|
||||||
# In the build directory
|
|
||||||
surge
|
|
||||||
```
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
|
|
||||||
- Free custom domain support
|
|
||||||
- Free SSL for surge.sh subdomains
|
|
||||||
- pushState support for single page apps
|
|
||||||
- Custom 404.html pages
|
|
||||||
- Barrier-free deployment through the CLI
|
|
||||||
- Easy integration into your Grunt toolchain
|
|
||||||
- Cross-origin resource support
|
|
||||||
- And more…
|
|
||||||
|
|
||||||
Learn more about [surge.sh on their website](https://surge.sh/)
|
|
||||||
|
|
||||||
### GitHub Pages
|
|
||||||
|
|
||||||
> WARNING! While great for project sites and light use, it is not advised to use
|
|
||||||
> GitHub Pages for production workloads. Please consider using a different
|
|
||||||
> service for mission critical applications.
|
|
||||||
|
|
||||||
> Websites for you and your projects. Hosted directly from your GitHub
|
|
||||||
> repository. Just edit, push, and your changes are live.
|
|
||||||
|
|
||||||
This deploy strategy makes more sense if you intend to maintain your project in
|
|
||||||
a GitHub repository. It allows you to specify a `branch` or `folder` as the
|
|
||||||
target for a GitHub Page's website. As you push code changes, the hosted content
|
|
||||||
updates to reflect those changes.
|
|
||||||
|
|
||||||
1. Head over to GitHub.com and create a new repository, or go to an existing
|
|
||||||
one. Click on the Settings tab.
|
|
||||||
2. Scroll down to the GitHub Pages section. Choose the `branch` or `folder` you
|
|
||||||
would like as the "root" of your website.
|
|
||||||
3. Fire up a browser and go to `http://username.github.io/repository`
|
|
||||||
|
|
||||||
Configuring Your Site:
|
|
||||||
|
|
||||||
- [Setting up a custom domain](https://help.github.com/en/articles/using-a-custom-domain-with-github-pages)
|
|
||||||
- [Setting up SSL](https://help.github.com/en/articles/securing-your-github-pages-site-with-https)
|
|
||||||
|
|
||||||
Learn more about [GitHub Pages on its website](https://pages.github.com/)
|
|
||||||
|
|
||||||
## Advanced
|
|
||||||
|
|
||||||
All of these options, while using providers with more service offerings,
|
|
||||||
demonstrate how to host the viewer with their respective file storage and CDN
|
|
||||||
offerings. While you can serve your static assets this way, if you're going
|
|
||||||
through the trouble of using AWS/GCP/Azure, it's more likely you're doing so to
|
|
||||||
avoid using a proxy or to simplify authentication.
|
|
||||||
|
|
||||||
If that is the case, check out some of our more advanced `docker` deployments
|
|
||||||
that target these providers from the left-hand sidepanel.
|
|
||||||
|
|
||||||
These guides can be a bit longer and a update more frequently. To provide
|
|
||||||
accurate documentation, we will link to each provider's own recommended steps:
|
|
||||||
|
|
||||||
### AWS S3 + Cloudfront
|
|
||||||
|
|
||||||
- [Host a Static Website](https://docs.aws.amazon.com/AmazonS3/latest/dev/website-hosting-custom-domain-walkthrough.html)
|
|
||||||
- [Speed Up Your Website with Cloudfront](https://docs.aws.amazon.com/AmazonS3/latest/dev/website-hosting-cloudfront-walkthrough.html)
|
|
||||||
|
|
||||||
### GCP + Cloudflare
|
|
||||||
|
|
||||||
- [Things to Know Before Getting Started](https://code.luasoftware.com/tutorials/google-cloud-storage/things-to-know-before-hosting-static-website-on-google-cloud-storage/)
|
|
||||||
- [Hosting a Static Website on GCP](https://cloud.google.com/storage/docs/hosting-static-website)
|
|
||||||
|
|
||||||
### Azure
|
|
||||||
|
|
||||||
- [Host a Static Website](https://docs.microsoft.com/en-us/azure/storage/blobs/storage-blob-static-website)
|
|
||||||
- [Add SSL Support](https://docs.microsoft.com/en-us/azure/storage/blobs/storage-https-custom-domain-cdn)
|
|
||||||
- [Configure a Custom Domain](https://docs.microsoft.com/en-us/azure/storage/blobs/storage-custom-domain-name)
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# User Account Control
|
|
||||||
|
|
||||||
> Coming soon - We are working on bringing the User Account Control to OHIF-v3
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
# Continous Integration (CI)
|
|
||||||
|
|
||||||
This repository uses `CircleCI` and `Netlify` for continous integration.
|
|
||||||
|
|
||||||
## Deploy Previews
|
|
||||||
|
|
||||||
[Netlify Deploy previews][deploy-previews] are generated for every pull request.
|
|
||||||
They allow pull request authors and reviewers to "Preview" the OHIF Viewer as if
|
|
||||||
the changes had been merged.
|
|
||||||
|
|
||||||
Deploy previews can be configured by modifying the `netlify.toml` file in the
|
|
||||||
root of the repository. Some additional scripts/assets for netlify are included
|
|
||||||
in the root `.netlify` directory.
|
|
||||||
|
|
||||||
## Workflows
|
|
||||||
|
|
||||||
[CircleCI Workflows][circleci-workflows] are a set of rules for defining a
|
|
||||||
collection of jobs and their run order. They are self-documenting and their
|
|
||||||
configuration can be found in our CircleCI configuration file:
|
|
||||||
`.circleci/config.yml`.
|
|
||||||
|
|
||||||
### Workflow: PR_CHECKS
|
|
||||||
|
|
||||||
The PR_CHECKS workflow (Pull Request Checks) runs our automated unit and
|
|
||||||
end-to-end tests for every code check-in. These tests must all pass before code
|
|
||||||
can be merged to our `master` branch.
|
|
||||||
|
|
||||||
<div style="text-align: center;">
|
|
||||||
<a href="/assets/img/WORKFLOW_PR_CHECKS.png">
|
|
||||||
<img src="/assets/img/WORKFLOW_PR_CHECKS.png" alt="workflow diagram" style="margin: 0 auto; max-width: 500px;" />
|
|
||||||
</a>
|
|
||||||
<div><i>Workflow diagram for PR_CHECKS</i></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
### Workflow: PR_OPTIONAL_DOCKER_PUBLISH
|
|
||||||
|
|
||||||
The PR_OPTIONAL_DOCKER_PUBLISH workflow allows for "manual approval" to publish
|
|
||||||
the pull request as a tagged docker image. This is helpful when changes need to
|
|
||||||
be tested with the Google Adapter before merging to `master`.
|
|
||||||
|
|
||||||
<div style="text-align: center;">
|
|
||||||
<a href="/assets/img/WORKFLOW_PR_OPTIONAL_DOCKER_PUBLISH.png">
|
|
||||||
<img src="/assets/img/WORKFLOW_PR_OPTIONAL_DOCKER_PUBLISH.png" alt="workflow diagram" style="margin: 0 auto; max-width: 500px;" />
|
|
||||||
</a>
|
|
||||||
<div><i>Workflow diagram for PR_WORKFLOW_PR_OPTIONAL_DOCKER_PUBLISH</i></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
> NOTE: This workflow will fail unless it's for a branch on our `upstream`
|
|
||||||
> repository. If you need this functionality, but the branch is from a fork,
|
|
||||||
> merge the changes to a short-lived `feature/` branch on `upstream`
|
|
||||||
|
|
||||||
### Workflow: DEPLOY
|
|
||||||
|
|
||||||
The DEPLOY workflow deploys the OHIF Viewer when changes are merged to master.
|
|
||||||
It uses the Netlify CLI to deploy assets created as part of the repository's PWA
|
|
||||||
Build process (`yarn run build`). The workflow allows for "Manual Approval" to
|
|
||||||
promote the build to `STAGING` and `PRODUCTION` environments.
|
|
||||||
|
|
||||||
<div style="text-align: center;">
|
|
||||||
<a href="/assets/img/WORKFLOW_DEPLOY.png">
|
|
||||||
<img src="/assets/img/WORKFLOW_DEPLOY.png" alt="workflow diagram" style="margin: 0 auto; max-width: 500px;" />
|
|
||||||
</a>
|
|
||||||
<div><i>Workflow diagram for WORKFLOW_DEPLOY</i></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
| Environment | Description | URL |
|
|
||||||
| ----------- | ---------------------------------------------------------------------------------- | --------------------------------------------- |
|
|
||||||
| Development | Always reflects latest changes on `master` branch. | [Netlify][netlify-dev] / [OHIF][ohif-dev] |
|
|
||||||
| Staging | For manual testing before promotion to prod. Keeps development workflow unblocked. | [Netlify][netlify-stage] / [OHIF][ohif-stage] |
|
|
||||||
| Production | Stable, tested, updated less frequently. | [Netlify][netlify-prod] / [OHIF][ohif-prod] |
|
|
||||||
|
|
||||||
### Workflow: RELEASE
|
|
||||||
|
|
||||||
The RELEASE workflow publishes our `npm` packages, updated documentation, and
|
|
||||||
`docker` image when changes are merged to master. `Lerna` and "Semantic Commit
|
|
||||||
Syntax" are used to independently version and publish the many packages in our
|
|
||||||
monorepository. If a new version is cut/released, a Docker image is created.
|
|
||||||
Documentation is generated with `gitbook` and pushed to our `gh-pages` branch.
|
|
||||||
GitHub hosts the `gh-pages` branch with GitHub Pages.
|
|
||||||
|
|
||||||
- Platform Packages: https://github.com/ohif/viewers/#platform
|
|
||||||
- Extension Packages: https://github.com/ohif/viewers/#extensions
|
|
||||||
- Documentation: https://docs.ohif.org/
|
|
||||||
|
|
||||||
<div style="text-align: center;">
|
|
||||||
<a href="/assets/img/WORKFLOW_RELEASE.png">
|
|
||||||
<img src="/assets/img/WORKFLOW_RELEASE.png" alt="workflow diagram" style="margin: 0 auto; max-width: 500px;" />
|
|
||||||
</a>
|
|
||||||
<div><i>Workflow diagram for WORKFLOW_RELEASE</i></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
### HOTFIX
|
|
||||||
|
|
||||||
_Not yet implemented_
|
|
||||||
|
|
||||||
<!--
|
|
||||||
LINKS
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
|
||||||
[deploy-previews]: https://www.netlify.com/blog/2016/07/20/introducing-deploy-previews-in-netlify/
|
|
||||||
[circleci-workflows]: https://circleci.com/docs/2.0/workflows/
|
|
||||||
[netlify-dev]: https://ohif-dev.netlify.com
|
|
||||||
[netlify-stage]: https://ohif-stage.netlify.com
|
|
||||||
[netlify-prod]: https://ohif-prod.netlify.com
|
|
||||||
[ohif-dev]: https://viewer-dev.ohif.org
|
|
||||||
[ohif-stage]: https://viewer-stage.ohif.org
|
|
||||||
[ohif-prod]: https://viewer-prod.ohif.org
|
|
||||||
<!-- prettier-ignore-end -->
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
# Contributing
|
|
||||||
|
|
||||||
## How can I help?
|
|
||||||
|
|
||||||
Fork the repository, make your change and submit a pull request. If you would
|
|
||||||
like to discuss the changes you intend to make to clarify where or how they
|
|
||||||
should be implemented, please don't hesitate to create a new issue. At a
|
|
||||||
minimum, you may want to read the following documentation:
|
|
||||||
|
|
||||||
- [Getting Started](/development/getting-started.md)
|
|
||||||
- [Architecture](/architecture/index.md)
|
|
||||||
|
|
||||||
Pull requests that are:
|
|
||||||
|
|
||||||
- Small
|
|
||||||
- [Well tested](./testing.md)
|
|
||||||
- Decoupled
|
|
||||||
|
|
||||||
Are much more likely to get reviewed and merged in a timely manner.
|
|
||||||
|
|
||||||
## When changes impact multiple repositories
|
|
||||||
|
|
||||||
While this can be tricky, we've tried to reduce how often this situation crops
|
|
||||||
up this with our [recent switch to a monorepo][monorepo]. Our maintained
|
|
||||||
extensions, ui components, internationalization library, and business logic can
|
|
||||||
all be developed by simply running `yarn run dev` from the repository root.
|
|
||||||
|
|
||||||
Testing the viewer with locally developed, unpublished package changes from a
|
|
||||||
package outside of the monorepo is most common with extension development. Let's
|
|
||||||
demonstrate how to accomplish this with two commonly forked extension
|
|
||||||
dependencies:
|
|
||||||
|
|
||||||
### `cornerstone-tools`
|
|
||||||
|
|
||||||
On your local file system:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# code/my-projects/
|
|
||||||
.
|
|
||||||
├── cornerstonejs/cornerstone-tools
|
|
||||||
└── ohif/viewers
|
|
||||||
```
|
|
||||||
|
|
||||||
- Open a terminal/shell
|
|
||||||
- Navigate to `cornerstonejs/cornerstone-tools`
|
|
||||||
- `yarn install`
|
|
||||||
- [`yarn link`](https://yarnpkg.com/en/docs/cli/link)
|
|
||||||
- `yarn run dev`
|
|
||||||
|
|
||||||
|
|
||||||
- Open a new terminal/shell
|
|
||||||
- Navigate to `ohif/viewers` (the root of ohif project)
|
|
||||||
- `yarn install`
|
|
||||||
- [`yarn link cornerstone-tools`](https://yarnpkg.com/en/docs/cli/link)
|
|
||||||
- `yarn run dev`
|
|
||||||
|
|
||||||
As you make changed to `cornerstone-tools`, and it's output is rebuilt, you
|
|
||||||
should see the following behavior:
|
|
||||||
|
|
||||||
<div style="text-align: center;">
|
|
||||||
<a href="/assets/img/cornerstone-tools-link.gif">
|
|
||||||
<img src="/assets/img/cornerstone-tools-link.gif" alt="Example of linked cornerstone-tools package" style="margin: 0 auto; max-width: 500px;" />
|
|
||||||
</a>
|
|
||||||
<div><i>example of linked cornerstone-tools package</i></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
If you wish to stop using your local package, run the following commands in the
|
|
||||||
`ohif/viewers` repository root:
|
|
||||||
|
|
||||||
- `yarn unlink cornerstone-tools`
|
|
||||||
- `yarn install --force`
|
|
||||||
|
|
||||||
<!--
|
|
||||||
### `react-vtkjs-viewport`
|
|
||||||
|
|
||||||
On your local file system:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# code/my-projects/
|
|
||||||
.
|
|
||||||
├── ohif/react-vtkjs-viewport
|
|
||||||
└── ohif/viewers
|
|
||||||
```
|
|
||||||
|
|
||||||
- Open a terminal/shell
|
|
||||||
- Navigate to `ohif/react-vtkjs-viewport`
|
|
||||||
- `yarn install`
|
|
||||||
- [`yarn link`](https://yarnpkg.com/en/docs/cli/link)
|
|
||||||
- `yarn run start`
|
|
||||||
- Open a new terminal/shell
|
|
||||||
- Navigate to `ohif/viewers`.
|
|
||||||
- `yarn install`
|
|
||||||
- [`yarn link react-vtkjs-viewport`](https://yarnpkg.com/en/docs/cli/link)
|
|
||||||
- `yarn run dev` -->
|
|
||||||
|
|
||||||
#### Other linkage notes
|
|
||||||
|
|
||||||
We're still working out some of the kinks with local package development as
|
|
||||||
there are a lot of factors that can influence the behavior of our development
|
|
||||||
server and bundler. If you encounter issues not addressed here, please don't
|
|
||||||
hesitate to reach out on GitHub.
|
|
||||||
|
|
||||||
Sometimes you might encounter a situation where the linking doesn't work as expected.
|
|
||||||
This might happen when there are multiple linked packages with the same name.
|
|
||||||
You can [remove][unlink] the linked packages inside yarn and try again.
|
|
||||||
|
|
||||||
## Any guidance on submitting changes?
|
|
||||||
|
|
||||||
While we do appreciate code contributions, triaging and integrating contributed
|
|
||||||
code changes can be very time consuming. Please consider the following tips when
|
|
||||||
working on your pull requests:
|
|
||||||
|
|
||||||
- Functionality is appropriate for the repository. Consider creating a GitHub
|
|
||||||
issue to discuss your suggested changes.
|
|
||||||
- The scope of the pull request is not too large. Please consider separate pull
|
|
||||||
requests for each feature as big pull requests are very time consuming to
|
|
||||||
understand.
|
|
||||||
|
|
||||||
We will provide feedback on your pull requests as soon as possible. Following
|
|
||||||
the tips above will help ensure your changes are reviewed.
|
|
||||||
|
|
||||||
<!-- ## Testing contribution pull requests
|
|
||||||
|
|
||||||
OHIF uses [netlify](https://www.netlify.com/) so that pull requests are
|
|
||||||
autogenerated and available for testing.
|
|
||||||
|
|
||||||
For example, [this url][example-url] allows you to test [pull request 237, the
|
|
||||||
request that created this FAQ entry,][pr-237] using data pulled from Amazon S3.
|
|
||||||
|
|
||||||
Replacing the number 237 in the link below with your pull request number should
|
|
||||||
let you test it as well and you can use this link for discussions on github
|
|
||||||
without requiring reviewers to download and build your branch.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
https://deploy-preview-237--ohif.netlify.com/viewer/?url=https://s3.eu-central-1.amazonaws.com/ohif-viewer/sampleDICOM.json
|
|
||||||
```
|
|
||||||
|
|
||||||
If you have made a documentation change, a link like this will let you preview
|
|
||||||
the gitbook generated by the pull request:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
https://deploy-preview-237--ohif.netlify.com/contributing.html
|
|
||||||
``` -->
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Links
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
|
||||||
[example-url]: https://deploy-preview-237--ohif.netlify.com/viewer/?url=https://s3.eu-central-1.amazonaws.com/ohif-viewer/sampleDICOM.json
|
|
||||||
[pr-237]: https://github.com/OHIF/Viewers/pull/237
|
|
||||||
[monorepo]: https://github.com/OHIF/Viewers/issues/768
|
|
||||||
[unlink]: https://stackoverflow.com/questions/58459698/is-there-a-command-to-unlink-all-yarn-packages-yarn-unlink-all
|
|
||||||
<!-- prettier-ignore-end -->
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
# Getting Started
|
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
### Fork & Clone
|
|
||||||
|
|
||||||
If you intend to contribute back changes, or if you would like to pull updates
|
|
||||||
we make to the OHIF Viewer, then follow these steps:
|
|
||||||
|
|
||||||
- [Fork][fork-a-repo] the [OHIF/Viewers][ohif-viewers-repo] repository
|
|
||||||
- [Create a local clone][clone-a-repo] of your fork
|
|
||||||
- `git clone https://github.com/YOUR-USERNAME/Viewers`
|
|
||||||
- Add OHIF/Viewers as a [remote repository][add-remote-repo] labled `upstream`
|
|
||||||
- Navigate to the cloned project's directory
|
|
||||||
- `git remote add upstream https://github.com/OHIF/Viewers.git`
|
|
||||||
|
|
||||||
With this setup, you can now [sync your fork][sync-changes] to keep it
|
|
||||||
up-to-date with the upstream (original) repository. This is called a "Triangular
|
|
||||||
Workflow" and is common for Open Source projects. The GitHub blog has a [good
|
|
||||||
graphic that illustrates this setup][triangular-workflow].
|
|
||||||
|
|
||||||
### Private
|
|
||||||
|
|
||||||
Alternatively, if you intend to use the OHIF Viewer as a starting point, and you
|
|
||||||
aren't as concerned with syncing updates, then follow these steps:
|
|
||||||
|
|
||||||
1. Navigate to the [OHIF/Viewers][ohif-viewers] repository
|
|
||||||
2. Click `Clone or download`, and then `Download ZIP`
|
|
||||||
3. Use the contents of the `.zip` file as a starting point for your viewer
|
|
||||||
|
|
||||||
> NOTE: It is still possible to sync changes using this approach. However,
|
|
||||||
> submitting pull requests for fixes and features are best done with the
|
|
||||||
> separate, forked repository setup described in "Fork & Clone"
|
|
||||||
|
|
||||||
## Developing
|
|
||||||
|
|
||||||
### Requirements
|
|
||||||
|
|
||||||
- [Node.js & NPM](https://nodejs.org/en/)
|
|
||||||
- [Yarn](https://yarnpkg.com/en/)
|
|
||||||
- Yarn workspaces should be enabled:
|
|
||||||
- `yarn config set workspaces-experimental true`
|
|
||||||
|
|
||||||
### Kick the tires
|
|
||||||
|
|
||||||
Navigate to the root of the project's directory in your terminal and run the
|
|
||||||
following commands:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Switch to the v3 branch
|
|
||||||
git switch feat/v2-main
|
|
||||||
|
|
||||||
# Restore dependencies
|
|
||||||
yarn install
|
|
||||||
|
|
||||||
# Start local development server
|
|
||||||
yarn run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
You should see the following output:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
@ohif/viewer: i 「wds」: Project is running at http://localhost:3000/
|
|
||||||
@ohif/viewer: i 「wds」: webpack output is served from /
|
|
||||||
@ohif/viewer: i 「wds」: Content not from webpack is served from D:\code\ohif\Viewers\platform\viewer
|
|
||||||
@ohif/viewer: i 「wds」: 404s will fallback to /index.html
|
|
||||||
|
|
||||||
# And a list of all generated files
|
|
||||||
```
|
|
||||||
|
|
||||||
### 🎉 Celebrate 🎉
|
|
||||||
|
|
||||||
<div style="padding:56.25% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/545988245?badge=0&autopause=0&player_id=0&app_id=58479" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen style="position:absolute;top:0;left:0;width:100%;height:100%;" title="getting-started"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
|
|
||||||
|
|
||||||
### Building for Production
|
|
||||||
|
|
||||||
> More comprehensive guides for building and publishing can be found in our
|
|
||||||
> [deployment docs](./../deployment/index.md)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build static assets to host a PWA
|
|
||||||
yarn run build
|
|
||||||
|
|
||||||
# Build packaged output (script-tag use)
|
|
||||||
yarn run build:package
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
- If you receive a _"No Studies Found"_ message and do not see your studies, try
|
|
||||||
changing the Study Date filters to a wider range.
|
|
||||||
- If you see a 'Loading' message which never resolves, check your browser
|
|
||||||
JavaScript console inside the Developer Tools to identify any errors.
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Links
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
|
||||||
[fork-a-repo]: https://help.github.com/en/articles/fork-a-repo
|
|
||||||
[clone-a-repo]: https://help.github.com/en/articles/fork-a-repo#step-2-create-a-local-clone-of-your-fork
|
|
||||||
[add-remote-repo]: https://help.github.com/en/articles/fork-a-repo#step-3-configure-git-to-sync-your-fork-with-the-original-spoon-knife-repository
|
|
||||||
[sync-changes]: https://help.github.com/en/articles/syncing-a-fork
|
|
||||||
[triangular-workflow]: https://github.blog/2015-07-29-git-2-5-including-multiple-worktrees-and-triangular-workflows/#improved-support-for-triangular-workflows
|
|
||||||
[ohif-viewers-repo]: https://github.com/OHIF/Viewers
|
|
||||||
[ohif-viewers]: https://github.com/OHIF/Viewers
|
|
||||||
<!-- prettier-ignore-end -->
|
|
||||||
@@ -1,211 +0,0 @@
|
|||||||
# Running Tests for OHIF
|
|
||||||
|
|
||||||
We introduce here various test types that is available
|
|
||||||
for OIHF, and how to run each test in order to make sure your contribution
|
|
||||||
hasn't broken any existing functionalities. Idea and philosophy of each testing category is discussed in the second
|
|
||||||
part of this page.
|
|
||||||
|
|
||||||
## Unit test
|
|
||||||
To run the unit test:
|
|
||||||
```
|
|
||||||
yarn run test:unit:ci
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: You should have already installed all the packages with `yarn install`.
|
|
||||||
|
|
||||||
|
|
||||||
Running unit test will generate a report at the end showing the successful and
|
|
||||||
unsuccessful tests with detailed explanations.
|
|
||||||
|
|
||||||
## End-to-end test
|
|
||||||
For running the OHIF e2e test you need to run the following steps:
|
|
||||||
|
|
||||||
- Create a mini-pacs for OHIF to access the images for testing. We download
|
|
||||||
and run our lightweight implementation which provides a collection of DICOM studies ([source code][mini-pacs]).
|
|
||||||
|
|
||||||
```
|
|
||||||
docker run -p 5985:5985 -p 5984:5984 -e USE_POUCHDB=true -e DB_SERVER=http://0.0.0.0 ohif/viewer-testdata:0.1-test
|
|
||||||
```
|
|
||||||
|
|
||||||
Successful execution should be
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
- Open a new terminal, navigate to the OHIF project, and run OHIF with the dicom-server config
|
|
||||||
|
|
||||||
```
|
|
||||||
APP_CONFIG=config/dicomweb-server.js yarn start
|
|
||||||
```
|
|
||||||
|
|
||||||
You should be able to see test studies in the study list
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
- Open a new terminal inside the OIHF project, and run the e2e cypress test
|
|
||||||
|
|
||||||
```
|
|
||||||
yarn run test:e2e
|
|
||||||
```
|
|
||||||
|
|
||||||
You should be able to see the cypress window open
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Run the tests by clicking on the `Run #number integration tests` .
|
|
||||||
|
|
||||||
A new window will open and you will see e2e tests being executed one after
|
|
||||||
each other.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Testing Philosiphy
|
|
||||||
|
|
||||||
> Testing is an opinionated topic. Here is a rough overview of our testing
|
|
||||||
> philosophy. See something you want to discuss or think should be changed? Open
|
|
||||||
> a PR and let's discuss.
|
|
||||||
|
|
||||||
You're an engineer. You know how to write code, and writing tests isn't all that
|
|
||||||
different. But do you know why we write tests? Do you know when to write one, or
|
|
||||||
what kind of test to write? How do you know if a test is a _"good"_ test? This
|
|
||||||
document's goal is to give you the tools you need to make those determinations.
|
|
||||||
|
|
||||||
Okay. So why do we write tests? To increase our... **CONFIDENCE**
|
|
||||||
|
|
||||||
- If I do a large refactor, does everything still work?
|
|
||||||
- If I changed some critical piece of code, is it safe to push to production?
|
|
||||||
|
|
||||||
Gaining the confidence we need to answer these questions after every change is
|
|
||||||
costly. Good tests allow us to answer them without manual regression testing.
|
|
||||||
What and how we choose to test to increase that confidence is nuanced.
|
|
||||||
|
|
||||||
## Kinds of Tests
|
|
||||||
|
|
||||||
Test's buy us confidence, but not all tests are created equal. Each kind of test
|
|
||||||
has a different cost to write and maintain. An expensive test is worth it if it
|
|
||||||
gives us confidence that a payment is processed, but it may not be the best
|
|
||||||
choice for asserting an element's border color.
|
|
||||||
|
|
||||||
| Test Type | Example | Speed | Cost |
|
|
||||||
| ----------- | ------------------------------------------------------------------------ | ---------------- | ------------------------------------------------------------------------ |
|
|
||||||
| Static | `addNums(1, '2')` called with `string`, expected `int`. | :rocket: Instant | :money_with_wings: |
|
|
||||||
| Unit | `addNums(1, 2)` returns expected result `3` | :airplane: Fast | :money_with_wings::money_with_wings: |
|
|
||||||
| Integration | Clicking "Sign In", navigates to the dashboard (mocked network requests) | :running: Okay | :money_with_wings::money_with_wings::money_with_wings: |
|
|
||||||
| End-to-end | Clicking "Sign In", navigates to the dashboard (no mocks) | :turtle: Slow | :money_with_wings::money_with_wings::money_with_wings::money_with_wings: |
|
|
||||||
|
|
||||||
- :rocket: Speed: How quickly tests run
|
|
||||||
- :money_with_wings: Cost: Time to write, and to debug when broken (more points
|
|
||||||
of failure)
|
|
||||||
|
|
||||||
### Static Code Analysis
|
|
||||||
|
|
||||||
Modern tooling gives us this "for free". It can catch invalid regular
|
|
||||||
expressions, unused variables, and guarantee we're calling methods/functions
|
|
||||||
with the expected paramater types.
|
|
||||||
|
|
||||||
Example Tooling:
|
|
||||||
|
|
||||||
- [ESLint][eslint-rules]
|
|
||||||
- [TypeScript][typescript-docs] or [Flow][flow-org]
|
|
||||||
|
|
||||||
### Unit Tests
|
|
||||||
|
|
||||||
The building blocks of our libraries and applications. For these, you'll often
|
|
||||||
be testing a single function or method. Conceptually, this equates to:
|
|
||||||
|
|
||||||
_Pure Function Test:_
|
|
||||||
|
|
||||||
- If I call `sum(2, 2)`, I expect the output to be `4`
|
|
||||||
|
|
||||||
_Side Effect Test:_
|
|
||||||
|
|
||||||
- If I call `resetViewport(viewport)`, I expect `cornerstone.reset` to be called
|
|
||||||
with `viewport`
|
|
||||||
|
|
||||||
#### When to use
|
|
||||||
|
|
||||||
Anything that is exposed as public API should have unit tests.
|
|
||||||
|
|
||||||
#### When to avoid
|
|
||||||
|
|
||||||
You're actually testing implementation details. You're testing implementation
|
|
||||||
details if:
|
|
||||||
|
|
||||||
- Your test does something that the consumer of your code would never do.
|
|
||||||
- IE. Using a private function
|
|
||||||
- A refactor can break your tests
|
|
||||||
|
|
||||||
### Integration Tests
|
|
||||||
|
|
||||||
We write integration tests to gain confidence that several units work together.
|
|
||||||
Generally, we want to mock as little as possible for these tests. In practice,
|
|
||||||
this means only mocking network requests.
|
|
||||||
|
|
||||||
#### When to use
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
### End-to-End Tests
|
|
||||||
|
|
||||||
These are the most expensive tests to write and maintain. Largely because, when
|
|
||||||
they fail, they have the largest number of potential points of failure. So why
|
|
||||||
do we write them? Because they also buy us the most confidence.
|
|
||||||
|
|
||||||
#### When to use
|
|
||||||
|
|
||||||
Mission critical features and functionality, or to cover a large breadth of
|
|
||||||
functionality until unit tests catch up. Unsure if we should have a test for
|
|
||||||
feature `X` or scenario `Y`? Open an issue and let's discuss.
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
- Does your test increase confidence?
|
|
||||||
- Does the test type chosen balance the cost-to-confidence ratio?
|
|
||||||
|
|
||||||
## Further Reading
|
|
||||||
|
|
||||||
### General
|
|
||||||
|
|
||||||
- [Assert(js) Conf 2018 Talks][assert-js-talks]
|
|
||||||
- [Write tests. Not too many. Mostly integration.][kent-talk] - Kent C. Dodds
|
|
||||||
- [I see your point, but…][gleb-talk] - Gleb Bahmutov
|
|
||||||
- [Static vs Unit vs Integration vs E2E Testing][kent-blog] - Kent C. Dodds
|
|
||||||
(Blog)
|
|
||||||
|
|
||||||
### End-to-end Testing w/ Cypress
|
|
||||||
|
|
||||||
- [Getting Started](https://docs.cypress.io/guides/overview/why-cypress.html)
|
|
||||||
- Be sure to check out `Getting Started` and `Core Concepts`
|
|
||||||
- [Best Practices](https://docs.cypress.io/guides/references/best-practices.html)
|
|
||||||
- [Example Recipes](https://docs.cypress.io/examples/examples/recipes.html)
|
|
||||||
|
|
||||||
## Testing Dorito
|
|
||||||
|
|
||||||
[![testing dorito][testing-dorito-img]][testing-dorito]
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Links
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
|
||||||
[eslint-rules]: https://eslint.org/docs/rules/
|
|
||||||
[mini-pacs]: https://github.com/OHIF/viewer-testdata
|
|
||||||
[typescript-docs]: https://www.typescriptlang.org/docs/home.html
|
|
||||||
[flow-org]: https://flow.org/
|
|
||||||
<!-- Talks -->
|
|
||||||
[assert-js-talks]: https://www.youtube.com/playlist?list=PLZ66c9_z3umNSrKSb5cmpxdXZcIPNvKGw
|
|
||||||
[kent-talk]: https://www.youtube.com/watch?v=Fha2bVoC8SE
|
|
||||||
[gleb-talk]: https://www.youtube.com/watch?v=5FnalKRjpZk
|
|
||||||
[kent-blog]: https://kentcdodds.com/blog/unit-vs-integration-vs-e2e-tests
|
|
||||||
<!-- Images -->
|
|
||||||
[testing-trophy]: https://twitter.com/kentcdodds/status/960723172591992832?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E960723172591992832&ref_url=https%3A%2F%2Fkentcdodds.com%2Fblog%2Fwrite-tests
|
|
||||||
[aaron-square]: https://twitter.com/Carofine247/status/966727489274961920
|
|
||||||
[gleb-pyramid]: https://twitter.com/Carofine247/status/966764532046684160/photo/3
|
|
||||||
[testing-pyramid]: https://dojo.ministryoftesting.com/dojo/lessons/the-mobile-test-pyramid
|
|
||||||
[testing-dorito]: https://twitter.com/denvercoder/status/960752578198843392
|
|
||||||
[testing-dorito-img]: https://pbs.twimg.com/media/DVVHXycUMAAcN-F?format=jpg&name=4096x4096
|
|
||||||
<!-- prettier-ignore-end -->
|
|
||||||
@@ -1,263 +0,0 @@
|
|||||||
# Extensions
|
|
||||||
|
|
||||||
- [Extensions](#extensions)
|
|
||||||
- [Overview](#overview)
|
|
||||||
- [Extension Skeleton](#extension-skeleton)
|
|
||||||
- [OHIF-Maintained Extensions](#ohif-maintained-extensions)
|
|
||||||
- [Registering an Extension](#registering-an-extension)
|
|
||||||
- [Registering at Runtime](#registering-at-runtime)
|
|
||||||
- [Registering at Build Time](#registering-at-build-time)
|
|
||||||
- [Lifecycle Hooks](#lifecycle-hooks)
|
|
||||||
- [Modules](#modules)
|
|
||||||
- [Contexts](#contexts)
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
We have re-designed the architecture of the `OHIF-v3` to enable building applications
|
|
||||||
that are easily extensible to various use cases (modes) that behind the scene would utilize desired functionalities (extensions) to reach the goal of the use case.
|
|
||||||
|
|
||||||
Previously, extensions were “additive” and could not easily be mixed and matched within the same viewer for different use cases. Previous `OHIF-v2` architecture meant that
|
|
||||||
any minor extension alteration usually would require the user to hard fork. E.g. removing some of the tools from the toolbar of the cornerstone extension meant you had to hard fork it, which was frustrating if the implementation was otherwise the same as master.
|
|
||||||
|
|
||||||
|
|
||||||
> - Developers should make packages of *reusable* functionality as extensions, and can consume
|
|
||||||
> publicly available extensions.
|
|
||||||
> - Any conceivable radiological workflow or viewer setup will be able to be built with the platform through *modes*.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Practical examples of extensions include:
|
|
||||||
|
|
||||||
- A set of segmentation tools that build on top of the `cornerstone` viewport
|
|
||||||
- A set of rendering functionalities to volume render the data
|
|
||||||
- [See our maintained extensions for more examples of what's possible](#maintained-extensions)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<mark>Diagram showing how extensions are configured and accessed.
|
|
||||||
<!--
|
|
||||||
<div style="text-align: center;">
|
|
||||||
<a href="/assets/img/extensions-diagram.png">
|
|
||||||
<img src="/assets/img/extensions-diagram.png" alt="Extensions Diagram" style="margin: 0 auto; max-width: 500px;" />
|
|
||||||
</a>
|
|
||||||
<div><i>Diagram showing how extensions are configured and accessed.</i></div>
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Extension Skeleton
|
|
||||||
|
|
||||||
An extension is a plain JavaScript object that has an `id` property, and one or
|
|
||||||
more [modules](#modules) and/or [lifecycle hooks](#lifecycle-hooks).
|
|
||||||
|
|
||||||
```js
|
|
||||||
// prettier-ignore
|
|
||||||
export default {
|
|
||||||
/**
|
|
||||||
* Only required property. Should be a unique value across all extensions.
|
|
||||||
*/
|
|
||||||
id: 'example-extension',
|
|
||||||
|
|
||||||
// Lifecyle
|
|
||||||
preRegistration() { /* */ },
|
|
||||||
onModeEnter() { /* */ },
|
|
||||||
onModeExit() { /* */ },
|
|
||||||
// Modules
|
|
||||||
getLayoutTemplateModule() { /* */ },
|
|
||||||
getDataSourcesModule() { /* */ },
|
|
||||||
getSopClassHandlerModule() { /* */ },
|
|
||||||
getPanelModule() { /* */ },
|
|
||||||
getViewportModule() { /* */ },
|
|
||||||
getCommandsModule() { /* */ },
|
|
||||||
getContextModule() { /* */ },
|
|
||||||
getToolbarModule() { /* */ },
|
|
||||||
getHangingProtocolModule() { /* */ },
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## OHIF-Maintained Extensions
|
|
||||||
A small number of powerful extensions for popular use cases are maintained by
|
|
||||||
OHIF. They're co-located in the [`OHIF/Viewers`][viewers-repo] repository, in
|
|
||||||
the top level [`extensions/`][ext-source] directory.
|
|
||||||
|
|
||||||
{% include "./_maintained-extensions-table.md" %}
|
|
||||||
|
|
||||||
|
|
||||||
## Registering an Extension
|
|
||||||
|
|
||||||
Extensions are building blocks that need to be registered. There are two different ways to register and configure extensions: At
|
|
||||||
[runtime](#registering-at-runtime) and at
|
|
||||||
[build time](#registering-at-build-time).
|
|
||||||
You can leverage one or both strategies. Which one(s) you choose depend on your
|
|
||||||
application's requirements.
|
|
||||||
|
|
||||||
Each [module](#modules) defined by the extension
|
|
||||||
becomes available to the modes via the `ExtensionManager` by requesting it via
|
|
||||||
its id. [Read more about Extension Manager](#extension-manager)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Registering at Runtime
|
|
||||||
|
|
||||||
The `@ohif/viewer` uses a [configuration file](../viewer/configuration.md) at
|
|
||||||
startup. The schema for that file includes an `extensions` key that supports an
|
|
||||||
array of extensions to register.
|
|
||||||
|
|
||||||
```js
|
|
||||||
import MyFirstExtension from '@ohif/extension-first'
|
|
||||||
import MySecondExtension from '@ohif/extension-second'
|
|
||||||
|
|
||||||
const extensionConfig = {/* extension configuration */}
|
|
||||||
|
|
||||||
// prettier-ignore
|
|
||||||
const config = {
|
|
||||||
routerBasename: '/',
|
|
||||||
extensions: [
|
|
||||||
MyFirstExtension,
|
|
||||||
[
|
|
||||||
MySecondExtension,
|
|
||||||
extensionConfig
|
|
||||||
],
|
|
||||||
],
|
|
||||||
modes: [/* modes */],
|
|
||||||
showStudyList: true,
|
|
||||||
dataSources: [ /* data source config */]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, behind the scene, the runtime-added extensions will get merged with the
|
|
||||||
default app extensions (note: default app extensions include: `OHIFDefaultExtension`,
|
|
||||||
`OHIFCornerstoneExtension`, `OHIFDICOMSRExtension`,
|
|
||||||
`OHIFMeasurementTrackingExtension`)
|
|
||||||
|
|
||||||
### Registering at Build Time
|
|
||||||
|
|
||||||
The `@ohif/viewer` works best when built as a "Progressive Web Application"
|
|
||||||
(PWA). If you know the extensions your application will need, you can specify
|
|
||||||
them at "build time" to leverage advantages afforded to us by modern tooling:
|
|
||||||
|
|
||||||
- Code Splitting (dynamic imports)
|
|
||||||
- Tree Shaking
|
|
||||||
- Dependency deduplication
|
|
||||||
|
|
||||||
You can update the list of bundled extensions by:
|
|
||||||
|
|
||||||
1. Having your `@ohif/viewer` project depend on the extension
|
|
||||||
2. Importing and adding it to the list of extensions in the
|
|
||||||
`<repo-root>/platform/src/index.js` entrypoint:
|
|
||||||
|
|
||||||
```js
|
|
||||||
import OHIFDefaultExtension from '@ohif/extension-default';
|
|
||||||
import OHIFCornerstoneExtension from '@ohif/extension-cornerstone';
|
|
||||||
import OHIFMeasurementTrackingExtension from '@ohif/extension-measurement-tracking';
|
|
||||||
import OHIFDICOMSRExtension from '@ohif/extension-dicom-sr';
|
|
||||||
import MyFirstExtension from '@ohif/extension-first'
|
|
||||||
|
|
||||||
/** Combine our appConfiguration and "baked-in" extensions */
|
|
||||||
const appProps = {
|
|
||||||
config: window ? window.config : {},
|
|
||||||
defaultExtensions: [
|
|
||||||
OHIFDefaultExtension,
|
|
||||||
OHIFCornerstoneExtension,
|
|
||||||
OHIFMeasurementTrackingExtension,
|
|
||||||
OHIFDICOMSRExtension,
|
|
||||||
MyFirstExtension
|
|
||||||
],
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## Lifecycle Hooks
|
|
||||||
|
|
||||||
Currently, there are three lifecycle hook for extensions:
|
|
||||||
|
|
||||||
|
|
||||||
[`preRegistration`](./lifecycle/pre-registration.md)
|
|
||||||
This hook is called once on initialization of the entire viewer application, used to initialize the extensions state, and consume user defined extension configuration. If an extension defines the [`preRegistration`](./lifecycle/pre-registration.md)
|
|
||||||
lifecycle hook, it is called before any modules are registered in the
|
|
||||||
`ExtensionManager`. It's most commonly used to wire up extensions to
|
|
||||||
[services](./../services/index.md) and [commands](./modules/commands.md), and to
|
|
||||||
bootstrap 3rd party libraries.
|
|
||||||
|
|
||||||
|
|
||||||
[`onModeEnter`](./lifecycle/on-mode-enter.md): This hook is called whenever a new mode is entered, or a mode’s data or datasource is switched. This hook can be used to initialize data.
|
|
||||||
|
|
||||||
[`onModeExit`](./lifecycle/on-mode-exit.md): Similarly to onModeEnter, this hook is called when navigating away from a mode, or before a mode’s data or datasource is changed. This can be used to clean up data (e.g. remove annotations that do not need to be persisted)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Modules
|
|
||||||
Modules are the meat of extensions, the `blocks` that we have been talking about a lot.
|
|
||||||
They provide "definitions", components, and filtering/mapping logic that are then made available to modes and services.
|
|
||||||
|
|
||||||
Each module type has a special purpose, and is consumed by our viewer
|
|
||||||
differently.
|
|
||||||
|
|
||||||
|
|
||||||
{% include "./_modules.md" %}
|
|
||||||
|
|
||||||
|
|
||||||
<figure style="text-align: center; font-style: italic;">Tbl. Module types with abridged descriptions and examples. Each module links to a dedicated documentation page.</figure>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Contexts
|
|
||||||
|
|
||||||
The `@ohif/viewer` tracks "active contexts" that extensions can use to scope
|
|
||||||
their functionality. Some example contexts being:
|
|
||||||
|
|
||||||
- Route: `ROUTE:VIEWER`, `ROUTE:STUDY_LIST`
|
|
||||||
- Active Viewport: `ACTIVE_VIEWPORT:CORNERSTONE`, `ACTIVE_VIEWPORT:VTK`
|
|
||||||
|
|
||||||
An extension module can use these to say "Only show this Toolbar Button if the
|
|
||||||
active viewport is a Cornerstone viewport." This helps us use the appropriate UI
|
|
||||||
and behaviors depending on the current contexts.
|
|
||||||
|
|
||||||
For example, if we have hotkey that "rotates the active viewport", each Viewport
|
|
||||||
module that supports this behavior can add a command with the same name, scoped
|
|
||||||
to the appropriate context. When the `command` is fired, the "active contexts"
|
|
||||||
are used to determine the appropriate implementation of the rotate behavior.
|
|
||||||
|
|
||||||
|
|
||||||
<!-- <mark>do we want the followings?
|
|
||||||
## Consuming Extensions
|
|
||||||
|
|
||||||
We consume extensions, via the `ExtensionManager`, in our `@ohif/viewer`
|
|
||||||
project.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const extensionManager = new ExtensionManager({
|
|
||||||
commandsManager,
|
|
||||||
servicesManager,
|
|
||||||
hotkeysManager
|
|
||||||
});
|
|
||||||
|
|
||||||
// prettier-ignore
|
|
||||||
extensionManager.registerExtensions([ /** **/ ]);
|
|
||||||
```
|
|
||||||
|
|
||||||
The `@ohif/viewer` project handles data fetching, basic routing, wires up UI
|
|
||||||
services, and is the home to the more bespoke application logic that doesn't
|
|
||||||
make as much sense to make reusable.
|
|
||||||
|
|
||||||
Long-term, replacing the `@ohif/viewer` application and consuming extensions
|
|
||||||
(and the `ExtensionManager`) in your own project is the ideal path for
|
|
||||||
applications requiring a high degree of customization that can't be achieved
|
|
||||||
with current theming, configuration, extension, and services support.
|
|
||||||
|
|
||||||
If you're not sure how to achieve your goals with the extensibility available
|
|
||||||
today, create a GitHub issue! -->
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!--
|
|
||||||
LINKS
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
|
||||||
[viewers-repo]: https://github.com/OHIF/Viewers
|
|
||||||
[ext-source]: https://github.com/OHIF/Viewers/tree/master/extensions
|
|
||||||
[module-types]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/extensions/MODULE_TYPES.js
|
|
||||||
<!-- prettier-ignore-end -->
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
# Lifecylce Hook: preRegistration
|
|
||||||
|
|
||||||
If an extension defines the `preRegistration` lifecycle hook, it is called
|
|
||||||
before any modules are registered in the `ExtensionManager`. This hook can be
|
|
||||||
used to:
|
|
||||||
|
|
||||||
- initialize 3rd party libraries
|
|
||||||
- register event listeners
|
|
||||||
- add or call services
|
|
||||||
- add or call commands
|
|
||||||
|
|
||||||
The `preRegistration` hook receives an object containing the
|
|
||||||
`ExtensionManager`'s associated `ServicesManager`, `CommandsManager`, and any
|
|
||||||
`configuration` that was provided with the extension at time of registration.
|
|
||||||
|
|
||||||
Example `preRegistration` implementation that register a new service and make it
|
|
||||||
available in the app. We will talk more in details for creating a new service for
|
|
||||||
`OHIF-v3`.
|
|
||||||
|
|
||||||
```js
|
|
||||||
|
|
||||||
// new service inside new extension
|
|
||||||
import MyNewService from './MyNewService';
|
|
||||||
|
|
||||||
export default function MyNewServiceWithServices(serviceManager) {
|
|
||||||
return {
|
|
||||||
name: 'MyNewService',
|
|
||||||
create: ({ configuration = {} }) => {
|
|
||||||
return new MyNewService(serviceManager);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
and
|
|
||||||
|
|
||||||
```js
|
|
||||||
import MyNewService from './MyNewService'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
id: 'MyExampleExtension',
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {object} params
|
|
||||||
* @param {object} params.configuration
|
|
||||||
* @param {ServicesManager} params.servicesManager
|
|
||||||
* @param {CommandsManager} params.commandsManager
|
|
||||||
* @returns void
|
|
||||||
*/
|
|
||||||
preRegistration({ servicesManager, commandsManager, configuration }) {
|
|
||||||
console.log('Wiring up important stuff.');
|
|
||||||
|
|
||||||
window.importantStuff = () => {
|
|
||||||
console.log(configuration);
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('Important stuff has been wired.');
|
|
||||||
window.importantStuff();
|
|
||||||
|
|
||||||
// Registering new services
|
|
||||||
servicesManager.registerService(MyNewService(servicesManager));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
```
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
# Module: Commands
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
- [Module: Commands](#module-commands)
|
|
||||||
- [Overview](#overview)
|
|
||||||
- [Command Definitions](#command-definitions)
|
|
||||||
- [Command Behavior](#command-behavior)
|
|
||||||
- [`CommandsManager` Public API](#commandsmanager-public-api)
|
|
||||||
- [Contexts](#contexts)
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
`CommandsModule` includes list of arbitrary functions. These may activate tools, communicate with a server, open a modal, etc.
|
|
||||||
The significant difference between `OHIF-v3` and `OHIF-v2` is that in `v3` a `mode` defines
|
|
||||||
its toolbar, and which commands each tool call is inside in its toolDefinition
|
|
||||||
|
|
||||||
An extension can register a Commands Module by defining a `getCommandsModule`
|
|
||||||
method. The Commands Module allows us to register one or more commands scoped to
|
|
||||||
specific [contexts](./../index.md#contexts). Commands have several unique
|
|
||||||
characteristics that make them tremendously powerful:
|
|
||||||
|
|
||||||
- Multiple implementations for the same command can be defined
|
|
||||||
- Only the correct command's implementation will be run, dependent on the
|
|
||||||
application's "context"
|
|
||||||
- Commands are used by hotkeys, toolbar buttons and render settings
|
|
||||||
|
|
||||||
Here is a simple example commands module:
|
|
||||||
|
|
||||||
```js
|
|
||||||
const getCommandsModule = () => ({
|
|
||||||
definitions: {
|
|
||||||
exampleActionDef: {
|
|
||||||
commandFn: ({ param1 }) => {
|
|
||||||
console.log(`param1's value is: ${param1}`);
|
|
||||||
},
|
|
||||||
// storeContexts: ['viewports'],
|
|
||||||
options: { param1: 'param1' },
|
|
||||||
context: 'VIEWER', // optional
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultContext: 'ACTIVE_VIEWPORT::DICOMSR',
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
Each definition returned by the Commands Module is registered to the
|
|
||||||
`ExtensionManager`'s `CommandsManager`.
|
|
||||||
|
|
||||||
> `storeContexts` has been removed in `OHIF-v3` and now modules have access to all commands and services. This change enables support for user-registered services.
|
|
||||||
|
|
||||||
## Command Definitions
|
|
||||||
|
|
||||||
The command definition consists of a named command (`exampleActionDef` below) and a
|
|
||||||
`commandFn`. The command name is used to call the command, and the `commandFn`
|
|
||||||
is the "command" that is actioned. T
|
|
||||||
|
|
||||||
```js
|
|
||||||
exampleActionDef: {
|
|
||||||
commandFn: ({ param1, options }) => { },
|
|
||||||
options: { param1: 'measurement' },
|
|
||||||
context: 'DEFAULT',
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Property | Type | Description |
|
|
||||||
| --------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
|
|
||||||
| `commandFn` | func | The function to call when command is run. Receives `options` and `storeContexts`. |
|
|
||||||
| `options` | object | (optional) Arguments to pass at the time of calling to the `commandFn` |
|
|
||||||
| `context` | string[] or string | (optional) Overrides the `defaultContext`. Let's us know if command is currently "available" to be run. |
|
|
||||||
|
|
||||||
## Command Behavior
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
**If there are multiple valid commands for the application's active contexts**
|
|
||||||
|
|
||||||
- What happens: all commands are run
|
|
||||||
- When to use: A `clearData` command that cleans up state for multiple
|
|
||||||
extensions
|
|
||||||
|
|
||||||
**If no commands are valid for the application's active contexts**
|
|
||||||
|
|
||||||
- What happens: a warning is printed to the console
|
|
||||||
- When to use: a `hotkey` (like "invert") that doesn't make sense for the
|
|
||||||
current viewport (PDF or HTML)
|
|
||||||
|
|
||||||
## `CommandsManager` Public API
|
|
||||||
|
|
||||||
If you would like to run a command in the consuming app or an extension, you can
|
|
||||||
use `CommandsManager.runCommand(commandName, options = {}, contextName)`
|
|
||||||
|
|
||||||
|
|
||||||
```js
|
|
||||||
// Returns all commands for a given context
|
|
||||||
commandsManager.getContext('string');
|
|
||||||
|
|
||||||
// Run a command, it will run all the `speak` commands in all contexts
|
|
||||||
commandsManager.runCommand('speak', { command: 'hello' });
|
|
||||||
|
|
||||||
// Run command, from Default context
|
|
||||||
commandsManager.runCommand('speak', { command: 'hello' }, ['DEFAULT']);
|
|
||||||
```
|
|
||||||
|
|
||||||
The `ExtensionManager` handles registering commands and creating contexts, so
|
|
||||||
most consumer's won't need these methods. If you find yourself using these, ask
|
|
||||||
yourself "why can't I register these commands via an extension?"
|
|
||||||
|
|
||||||
```js
|
|
||||||
// Used by the `ExtensionManager` to register new commands
|
|
||||||
commandsManager.registerCommand('context', 'name', commandDefinition);
|
|
||||||
|
|
||||||
// Creates a new context; clears the context if it already exists
|
|
||||||
commandsManager.createContext('string');
|
|
||||||
```
|
|
||||||
|
|
||||||
### Contexts
|
|
||||||
|
|
||||||
It is up to the consuming application to define what contexts are possible, and
|
|
||||||
which ones are currently active. As extensions depend heavily on these, we will
|
|
||||||
likely publish guidance around creating contexts, and ways to override extension
|
|
||||||
defined contexts in the near future. If you would like to discuss potential
|
|
||||||
changes to how contexts work, please don't hesitate to create a new GitHub
|
|
||||||
issue.
|
|
||||||
|
|
||||||
[Some additional information on Contexts can be found here.](./../index.md#contexts)
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
# Module: Panel
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
UI that is intended to be displayed within a panel. The default LayoutTemplate has panels on the left and right sides, however one could make a template with panels at the top or bottom and make extensions with panels intended for such slots.
|
|
||||||
|
|
||||||
|
|
||||||
An extension can register a Panel Module by defining a `getPanelModule` method.
|
|
||||||
The panel module provides the ability to define `menuOptions` and `components`
|
|
||||||
that can be used by the consuming application. `components` are React Components
|
|
||||||
that can be displayed in the consuming application's "Panel" Component.
|
|
||||||
|
|
||||||
|
|
||||||
<!-- <center><i>A panel extension example</i></center> -->
|
|
||||||
|
|
||||||
The `menuOptions`'s `target` key points to a registered `components`'s `id`. A
|
|
||||||
`defaultContext` is applied to all `menuOption`s; however, each `menuOption` can
|
|
||||||
optional provide it's own `context` value.
|
|
||||||
|
|
||||||
The `getPanelModule` receives an object containing the `ExtensionManager`'s
|
|
||||||
associated `ServicesManager` and `CommandsManager`.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
```js
|
|
||||||
import PanelMeasurementTable from './PanelMeasurementTable.js';
|
|
||||||
|
|
||||||
|
|
||||||
function getPanelModule({
|
|
||||||
commandsManager,
|
|
||||||
extensionManager,
|
|
||||||
servicesManager,
|
|
||||||
}) {
|
|
||||||
const wrappedMeasurementPanel = () => {
|
|
||||||
return (
|
|
||||||
<PanelMeasurementTable
|
|
||||||
commandsManager={commandsManager}
|
|
||||||
servicesManager={servicesManager}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: 'measure',
|
|
||||||
iconName: 'list-bullets',
|
|
||||||
iconLabel: 'Measure',
|
|
||||||
label: 'Measurements',
|
|
||||||
isDisabled: studies => {}, // optional
|
|
||||||
component: wrappedMeasurementPanel,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Consuming Panels Inside Modes
|
|
||||||
As explained earlier, extensions make the functionalities and components available and
|
|
||||||
`modes` utilize them to build an app. So, as seen above, we are not actually defining
|
|
||||||
which side the panel should be opened. Our extension is providing the component with
|
|
||||||
its.
|
|
||||||
|
|
||||||
New: You can easily add multiple panels to the left/right side of the viewer using the mode
|
|
||||||
configuration. As seen below, the `leftPanels` and `rightPanels` accept an `Array` of
|
|
||||||
the `IDs`.
|
|
||||||
|
|
||||||
```js
|
|
||||||
export default function mode({ modeConfiguration }) {
|
|
||||||
return {
|
|
||||||
id: "viewer",
|
|
||||||
routes: [
|
|
||||||
{
|
|
||||||
path: "longitudinal",
|
|
||||||
layoutTemplate: ({ location, servicesManager }) => {
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
props: {
|
|
||||||
leftPanels: [
|
|
||||||
"org.ohif.measurement-tracking.panelModule.seriesList",
|
|
||||||
],
|
|
||||||
rightPanels: [
|
|
||||||
"org.ohif.measurement-tracking.panelModule.trackedMeasurements",
|
|
||||||
],
|
|
||||||
viewports,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
extensions: [
|
|
||||||
"org.ohif.default",
|
|
||||||
"org.ohif.cornerstone",
|
|
||||||
"org.ohif.measurement-tracking",
|
|
||||||
"org.ohif.dicom-sr",
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
# Module: SOP Class Handler
|
|
||||||
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
This module defines how a specific DICOM SOP class should be processed to make a displaySet, something that can be hung in a viewport. An extension can register a [SOP Class][sop-class-link] Handler Module by defining a `getSopClassHandlerModule` method. The [SOP Class][sop-class-link].
|
|
||||||
|
|
||||||
The mode chooses what SOPClassHandlers to use, so you could process a series in a different way depending on mode within the same application.
|
|
||||||
|
|
||||||
|
|
||||||
SOPClassHandler is a bit different from the other modules, as it doesn't provide a `1:1`
|
|
||||||
schema for UI or provide it's own components. It instead defines:
|
|
||||||
|
|
||||||
- `sopClassUIDs`: an array of string SOP Class UIDs that the
|
|
||||||
`getDisplaySetFromSeries` method should be applied to.
|
|
||||||
- `getDisplaySetFromSeries`: a method that maps series and study metadata to a
|
|
||||||
display set
|
|
||||||
|
|
||||||
A `displaySet` has the following shape:
|
|
||||||
|
|
||||||
```js
|
|
||||||
return {
|
|
||||||
Modality: 'MR',
|
|
||||||
displaySetInstanceUIDD
|
|
||||||
SeriesDate,
|
|
||||||
SeriesTime,
|
|
||||||
SeriesInstanceUID,
|
|
||||||
StudyInstanceUID,
|
|
||||||
SeriesNumber,
|
|
||||||
FrameRate,
|
|
||||||
SeriesDescription,
|
|
||||||
isMultiFrame,
|
|
||||||
numImageFrames,
|
|
||||||
SOPClassHandlerId,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Example SOP Class Handler Module
|
|
||||||
|
|
||||||
```js
|
|
||||||
import ImageSet from '@ohif/core/src/classes/ImageSet';
|
|
||||||
|
|
||||||
|
|
||||||
const sopClassDictionary = {
|
|
||||||
CTImageStorage: "1.2.840.10008.5.1.4.1.1.2",
|
|
||||||
MRImageStorage: "1.2.840.10008.5.1.4.1.1.4",
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// It is important to note that the used SOPClassUIDs in the modes are in the order that is specified in the array.
|
|
||||||
const sopClassUids = [
|
|
||||||
sopClassDictionary.CTImageStorage,
|
|
||||||
sopClassDictionary.MRImageStorage,
|
|
||||||
;
|
|
||||||
|
|
||||||
const makeDisplaySet = (instances) => {
|
|
||||||
const instance = instances[0];
|
|
||||||
const imageSet = new ImageSet(instances);
|
|
||||||
|
|
||||||
imageSet.setAttributes({
|
|
||||||
displaySetInstanceUID: imageSet.uid,
|
|
||||||
SeriesDate: instance.SeriesDate,
|
|
||||||
SeriesTime: instance.SeriesTime,
|
|
||||||
SeriesInstanceUID: instance.SeriesInstanceUID,
|
|
||||||
StudyInstanceUID: instance.StudyInstanceUID,
|
|
||||||
SeriesNumber: instance.SeriesNumber,
|
|
||||||
FrameRate: instance.FrameTime,
|
|
||||||
SeriesDescription: instance.SeriesDescription,
|
|
||||||
Modality: instance.Modality,
|
|
||||||
isMultiFrame: isMultiFrame(instance),
|
|
||||||
numImageFrames: instances.length,
|
|
||||||
SOPClassHandlerId: `${id}.sopClassHandlerModule.${sopClassHandlerName}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
return imageSet;
|
|
||||||
};
|
|
||||||
|
|
||||||
getSopClassHandlerModule = () => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: 'stack,
|
|
||||||
sopClassUids,
|
|
||||||
getDisplaySetsFromSeries: makeDisplaySet,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### More examples :
|
|
||||||
You can find another example for this mapping between raw metadata and displaySet for
|
|
||||||
`DICOM-SR` extension.
|
|
||||||
|
|
||||||
## `@ohif/viewer` usage
|
|
||||||
|
|
||||||
We use the `sopClassHandlerModule`s in `DisplaySetService` where we
|
|
||||||
transform instances from the raw metadata format to a OHIF displaySet format.
|
|
||||||
You can read more about DisplaySetService here.
|
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
|
||||||
[sop-class-link]: http://dicom.nema.org/dicom/2013/output/chtml/part04/sect_B.5.html
|
|
||||||
[dicom-html-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-html/src/OHIFDicomHtmlSopClassHandler.js#L4-L12
|
|
||||||
[dicom-pdf-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-pdf/src/OHIFDicomPDFSopClassHandler.js#L4-L6
|
|
||||||
[dicom-micro-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-microscopy/src/DicomMicroscopySopClassHandler.js#L5-L7
|
|
||||||
[dicom-seg-sop]: https://github.com/OHIF/Viewers/blob/master/extensions/dicom-segmentation/src/OHIFDicomSegSopClassHandler.js#L5-L7
|
|
||||||
<!-- prettier-ignore-end -->
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
# Module: Toolbar
|
|
||||||
|
|
||||||
An extension can register a Toolbar Module by defining a `getToolbarModule`
|
|
||||||
method. `OHIF-v3`'s `default` extension (`"ohif.org.default"`) provides 5 main toolbar button types:
|
|
||||||
|
|
||||||
- [Module: Toolbar](#module-toolbar)
|
|
||||||
- [Example Toolbar Module](#example-toolbar-module)
|
|
||||||
- [Toolbar buttons consumed in modes](#toolbar-buttons-consumed-in-modes)
|
|
||||||
- [Button Definitions](#button-definitions)
|
|
||||||
- [Nested Buttons](#nested-buttons)
|
|
||||||
- [Layout Template](#layout-template)
|
|
||||||
- [Custom Button](#custom-button)
|
|
||||||
- [Custom tool](#custom-tool)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Example Toolbar Module
|
|
||||||
The Toolbar Module should return an array of `objects`. There are currently a few different variations of definitions,
|
|
||||||
each one is detailed further down.
|
|
||||||
|
|
||||||
```js
|
|
||||||
export default function getToolbarModule({ commandsManager, servicesManager }) {
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: 'ohif.divider',
|
|
||||||
defaultComponent: ToolbarDivider,
|
|
||||||
clickHandler: () => {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ohif.action',
|
|
||||||
defaultComponent: ToolbarButton,
|
|
||||||
clickHandler: () => {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ohif.radioGroup',
|
|
||||||
defaultComponent: ToolbarButton,
|
|
||||||
clickHandler: () => {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ohif.splitButton',
|
|
||||||
defaultComponent: ToolbarSplitButton,
|
|
||||||
clickHandler: () => {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ohif.layoutSelector',
|
|
||||||
defaultComponent: ToolbarLayoutSelector,
|
|
||||||
clickHandler: (evt, clickedBtn, btnSectionName) => {},
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Toolbar buttons consumed in modes
|
|
||||||
Below we can see a simplified version of the `longitudinal` mode that shows how
|
|
||||||
a mode can add buttons to the toolbar by calling `ToolBarService.addButtons(toolbarButtons)`.
|
|
||||||
`toolbarButtons` is an array of `toolDefinitions` which we will learn next.
|
|
||||||
|
|
||||||
|
|
||||||
```js
|
|
||||||
export default function mode({ modeConfiguration }) {
|
|
||||||
return {
|
|
||||||
id: 'viewer',
|
|
||||||
displayName: 'Basic Viewer',
|
|
||||||
|
|
||||||
onModeEnter: ({ servicesManager, extensionManager }) => {
|
|
||||||
const { ToolBarService } = servicesManager.services;
|
|
||||||
|
|
||||||
ToolBarService.init(extensionManager);
|
|
||||||
ToolBarService.addButtons(toolbarButtons);
|
|
||||||
},
|
|
||||||
routes: [
|
|
||||||
{
|
|
||||||
path: 'longitudinal',
|
|
||||||
layoutTemplate: ({ location, servicesManager }) => {
|
|
||||||
return {/* */};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
extensions: [
|
|
||||||
'org.ohif.default',
|
|
||||||
'org.ohif.cornerstone',
|
|
||||||
'org.ohif.measurement-tracking',
|
|
||||||
'org.ohif.dicom-sr',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Button Definitions
|
|
||||||
|
|
||||||
The simplest toolbarButtons definition has the following properties:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
id: 'Zoom',
|
|
||||||
type: 'ohif.radioGroup',
|
|
||||||
props: {
|
|
||||||
type: 'tool',
|
|
||||||
icon: 'tool-zoom',
|
|
||||||
label: 'Zoom',
|
|
||||||
commandOptions: { toolName: 'Zoom' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
| property | description | values |
|
|
||||||
| ---------------- | ----------------------------------------------------------------- | ----------------------------------------- |
|
|
||||||
| `id` | Unique string identifier for the definition | \* |
|
|
||||||
| `label` | User/display friendly to show in UI | \* |
|
|
||||||
| `icon` | A string name for an icon supported by the consuming application. | \* |
|
|
||||||
| `type` | Used to determine the button's behaviour | "tool", "toggle", "action" |
|
|
||||||
| `commandName` | (optional) The command to run when the button is used. | Any command registered by a `CommandModule` |
|
|
||||||
| `commandOptions` | (optional) Options to pass the target `commandName` | \* |
|
|
||||||
|
|
||||||
|
|
||||||
There are three main types of toolbar buttons:
|
|
||||||
|
|
||||||
- `tool`: buttons that enable a tool by running the `setToolActive` command with the `commandOptions`
|
|
||||||
- `toggle`: buttons that acts as a toggle: e.g., linking viewports
|
|
||||||
- `action`: buttons that executes an action: e.g., capture button to save screenshot
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Nested Buttons
|
|
||||||
You can use the `ohif.splitButton` type to build a button with extra tools in the dropdown.
|
|
||||||
|
|
||||||
- First you need to give your `primary` tool definition to the split button
|
|
||||||
- the `secondary` properties can be a simple arrow down (`chevron-down` icon)
|
|
||||||
- For adding the extra tools add them to the `items` list.
|
|
||||||
|
|
||||||
You can see below how `longitudinal` mode is using the available toolbarModule to create
|
|
||||||
`MeasurementTools` nested button
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
```js
|
|
||||||
// modes/longitudinal/src/toolbarButtons.js
|
|
||||||
|
|
||||||
{
|
|
||||||
id: 'MeasurementTools',
|
|
||||||
type: 'ohif.splitButton',
|
|
||||||
props: {
|
|
||||||
groupId: 'MeasurementTools',
|
|
||||||
isRadio: true,
|
|
||||||
primary: {
|
|
||||||
id: 'Length',
|
|
||||||
icon: 'tool-length',
|
|
||||||
label: 'Length',
|
|
||||||
type: 'tool',
|
|
||||||
commandOptions: {
|
|
||||||
toolName: 'Length',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
secondary: {
|
|
||||||
icon: 'chevron-down',
|
|
||||||
label: '',
|
|
||||||
isActive: true,
|
|
||||||
tooltip: 'More Measure Tools',
|
|
||||||
},
|
|
||||||
items: [
|
|
||||||
// Length tool
|
|
||||||
{
|
|
||||||
id: 'Length',
|
|
||||||
icon: 'tool-length',
|
|
||||||
label: 'Length',
|
|
||||||
type: 'tool',
|
|
||||||
commandOptions: {
|
|
||||||
toolName: 'Length',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Bidirectional tool
|
|
||||||
{
|
|
||||||
id: 'Bidirectional',
|
|
||||||
icon: 'tool-bidirectional',
|
|
||||||
label: 'Length',
|
|
||||||
type: 'tool',
|
|
||||||
commandOptions: {
|
|
||||||
toolName: 'Bidirectional',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Ellipse tool
|
|
||||||
{
|
|
||||||
id: 'EllipticalRoi',
|
|
||||||
icon: 'tool-elipse',
|
|
||||||
label: 'Ellipse',
|
|
||||||
type: 'tool',
|
|
||||||
commandOptions: {
|
|
||||||
toolName: 'EllipticalRoi',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
<div style="padding:62.5% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/547957214?badge=0&autopause=0&player_id=0&app_id=58479" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen style="position:absolute;top:0;left:0;width:100%;height:100%;" title="Toolbar"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
|
|
||||||
|
|
||||||
## Layout Template
|
|
||||||
Layout selector button and logic is also provided by the OHIF-v3 `default` extension.
|
|
||||||
To use it, you can just add the following definition to the list of `toolDefinitions`
|
|
||||||
|
|
||||||

|
|
||||||
```js
|
|
||||||
{
|
|
||||||
id: 'Layout',
|
|
||||||
type: 'ohif.layoutSelector',
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
<div style="padding:56.25% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/545993263?badge=0&autopause=0&player_id=0&app_id=58479" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen style="position:absolute;top:0;left:0;width:100%;height:100%;" title="Viewer-layout"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
|
|
||||||
|
|
||||||
|
|
||||||
## Custom Button
|
|
||||||
You can also create your own extension, and add your new custom tool appearance (e.g., split horizantlly instead of vertically for split tool).
|
|
||||||
Simply add `getToolbarModule` to your extension, and pass your tool react component to its
|
|
||||||
`defaultComponent` property in the returned object.
|
|
||||||
You can use `@ohif/ui` components such as `IconButton, Icon, Tooltip, ToolbarButton` to
|
|
||||||
build your own component.
|
|
||||||
|
|
||||||
|
|
||||||
```js
|
|
||||||
import myToolComponent from './myToolComponent'
|
|
||||||
|
|
||||||
|
|
||||||
export default function getToolbarModule({ commandsManager, servicesManager }) {
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: 'new-tool-type',
|
|
||||||
defaultComponent: myToolComponent,
|
|
||||||
clickHandler: () => {},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Custom tool
|
|
||||||
<mark> I want to create a new tool
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
# Module: Viewport
|
|
||||||
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
Viewports consume a displaySet and display/allow the user to interact with data. An extension can register a Viewport Module by defining a `getViewportModule`
|
|
||||||
method that returns a React component. Currently, we use viewport components to
|
|
||||||
add support for:
|
|
||||||
|
|
||||||
- 2D Medical Image Viewing (cornerstone ext.)
|
|
||||||
- Structured Reports as SR (DICOM SR ext.)
|
|
||||||
- Structured Reports as HTML (DICOM html ext.)
|
|
||||||
- Encapsulated PDFs as PDFs (DICOM pdf ext.)
|
|
||||||
- Whole Slide Microscopy Viewing (whole slide ext.)
|
|
||||||
- etc.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
The general pattern is that a mode can define which `Viewport` to use for which specific
|
|
||||||
`SOPClassHandlerUID`, so if you want to fork just a single Viewport component for a specialized mode, this is possible.
|
|
||||||
|
|
||||||
```JSX
|
|
||||||
// displaySet, viewportIndex, dataSource
|
|
||||||
const getViewportModule = () => {
|
|
||||||
const wrappedViewport = props => {
|
|
||||||
return (
|
|
||||||
<ExampleViewport
|
|
||||||
{...props}
|
|
||||||
onEvent={data => {
|
|
||||||
commandsManager.runCommand('commandName', data);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return [{ name: 'example', component: wrappedViewport }];
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Example Viewport Component
|
|
||||||
A simplified version of the tracked CornerstoneViewport is shown below, which creates
|
|
||||||
a cornerstone viewport and and action bar on top of it.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
```JSX
|
|
||||||
function TrackedCornerstoneViewport({
|
|
||||||
children,
|
|
||||||
dataSource,
|
|
||||||
displaySet,
|
|
||||||
viewportIndex,
|
|
||||||
servicesManager,
|
|
||||||
extensionManager,
|
|
||||||
commandsManager,
|
|
||||||
}) {
|
|
||||||
const renderViewport = () => {
|
|
||||||
const { component: Component } = extensionManager.getModuleEntry(
|
|
||||||
'org.ohif.cornerstone.viewportModule.cornerstone'
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
<Component
|
|
||||||
onElementEnabled={onElementEnabled}
|
|
||||||
element={element}
|
|
||||||
{...props}
|
|
||||||
></Component>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewportActionBar onDoubleClick onSeriesChange />
|
|
||||||
<div className="relative flex flex-row w-full h-full overflow-hidden">
|
|
||||||
{renderViewport()}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||

|
|
||||||
### `@ohif/viewer`
|
|
||||||
|
|
||||||
Viewport components are managed by the `ViewportGrid` Component. Which Viewport
|
|
||||||
component is used depends on:
|
|
||||||
|
|
||||||
- Hanging Protocols
|
|
||||||
- The Layout Configuration
|
|
||||||
- Registered SopClassHandlers
|
|
||||||
|
|
||||||
|
|
||||||

|
|
||||||
<center><i>An example of three cornerstone Viewports</i></center>
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
# Browser Support
|
|
||||||
|
|
||||||
The browsers that we support are specified in the `.browserlistrc` file located
|
|
||||||
in the `platform/viewer` project. While we leverage the latest language features
|
|
||||||
when writing code, we rely on `babel` to _transpile_ our code so that it can run
|
|
||||||
in the browsers that we support.
|
|
||||||
|
|
||||||
## In Practice
|
|
||||||
|
|
||||||
The OHIF Viewer is capable of _running_ on:
|
|
||||||
|
|
||||||
- IE 11
|
|
||||||
- FireFox
|
|
||||||
- Chrome
|
|
||||||
- Safari
|
|
||||||
- Edge
|
|
||||||
|
|
||||||
However, we do not have the resources to adequately test and maintain bug free
|
|
||||||
functionality across all of these. In order to push web based medical imaging
|
|
||||||
forward, we focus our development efforts on recent version of modern evergreen
|
|
||||||
browsers.
|
|
||||||
|
|
||||||
Our support of older browsers equates to our willingness to review PRs for bug
|
|
||||||
fixes, and target their minimum JS support whenever possible.
|
|
||||||
|
|
||||||
### Polyfills
|
|
||||||
|
|
||||||
> A polyfill, or polyfiller, is a piece of code (or plugin) that provides the
|
|
||||||
> technology that you, the developer, expect the browser to provide natively.
|
|
||||||
|
|
||||||
An example of a polyfill is that you expect `Array.prototype.filter` to exist,
|
|
||||||
but for some reason, the browser that's being used has not implemented that
|
|
||||||
language feature yet. Our earlier transpilation will rectify _syntax_
|
|
||||||
discrepencies, but unimplemented features require a "temporary" implementation.
|
|
||||||
That's where polyfills step in.
|
|
||||||
|
|
||||||
You can utilize a service like [polyfill.io](https://polyfill.io/v3/) to
|
|
||||||
auto-detect and apply polyfills as needed, or you can update the PWA build to
|
|
||||||
include polyfill's in your bundle by incorporating [core-js][core-js]
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Links
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
|
||||||
[core-js]: https://github.com/zloirock/core-js/blob/master/docs/2019-03-19-core-js-3-babel-and-a-look-into-the-future.md
|
|
||||||
<!-- prettier-ignore-end -->
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
# Frequently Asked Questions
|
|
||||||
|
|
||||||
## Index
|
|
||||||
|
|
||||||
- [Report a bug][report-bug]
|
|
||||||
- [Request a feature][new-feature]
|
|
||||||
- [Commercial Support & Consulting][commercial-support]
|
|
||||||
- [Academic collaborations][academic]
|
|
||||||
- [FDA Clearance or CE Marking][fda-clearance]
|
|
||||||
- [HIPAA Compliance][hipaa]
|
|
||||||
|
|
||||||
### How do I report a bug?
|
|
||||||
|
|
||||||
Navigate to our [GitHub Repository][new-issue], and submit a new bug report.
|
|
||||||
Follow the steps outlined in the [Bug Report Template][bug-report-template].
|
|
||||||
|
|
||||||
### How can I request a new feature?
|
|
||||||
|
|
||||||
At the moment we are in the process of defining our roadmap and will do our best
|
|
||||||
to communicate this to the community. If your requested feature is on the
|
|
||||||
roadmap, then it will most likely be built at some point. If it is not, you are
|
|
||||||
welcome to build it yourself and [contribute it](development/contributing.md).
|
|
||||||
If you have resources and would like to fund the development of a feature,
|
|
||||||
please [contact us](http://www.ohif.org) or work with community members that
|
|
||||||
offer [consulting services][commercial-support].
|
|
||||||
|
|
||||||
### Who should I contact about Academic Collaborations?
|
|
||||||
|
|
||||||
[Gordon J. Harris](http://www.dfhcc.harvard.edu/insider/member-detail/member/gordon-j-harris-phd/)
|
|
||||||
at Massachusetts General Hospital is the primary contact for any academic
|
|
||||||
collaborators. We are always happy to hear about new groups interested in using
|
|
||||||
the OHIF framework, and may be able to provide development support if the
|
|
||||||
proposed collaboration has an impact on cancer research.
|
|
||||||
|
|
||||||
### Does OHIF offer commercial support?
|
|
||||||
|
|
||||||
The Open Health Imaging Foundation does not offer commercial support, however,
|
|
||||||
some community members do offer consulting services. The following contacts may
|
|
||||||
be useful:
|
|
||||||
|
|
||||||
- Rob Lewis ([Radical Imaging](http://radicalimaging.com/))
|
|
||||||
|
|
||||||
**Please file a Pull Request if you wish to add your name or organization to
|
|
||||||
this list.**
|
|
||||||
|
|
||||||
### Does The OHIF Viewer have [510(k) Clearance][501k-clearance] from the U.S. F.D.A or [CE Marking][ce-marking] from the European Commission?
|
|
||||||
|
|
||||||
**NO.** The OHIF Viewer is **NOT** F.D.A. cleared or CE Marked. It is the users
|
|
||||||
responsibility to ensure compliance with applicable rules and regulations. The
|
|
||||||
[License](https://github.com/OHIF/Viewers/blob/master/LICENSE) for the OHIF
|
|
||||||
Platform does not prevent your company or group from seeking F.D.A. clearance
|
|
||||||
for a product built using the platform.
|
|
||||||
|
|
||||||
If you have gone this route (or are going there), please let us know because we
|
|
||||||
would be interested to hear about your experience.
|
|
||||||
|
|
||||||
### Is The OHIF Viewer [HIPAA][hipaa-def] Compliant?
|
|
||||||
|
|
||||||
**NO.** The OHIF Viewer **DOES NOT** fulfill all of the criteria to become HIPAA
|
|
||||||
Compliant. It is the users responsibility to ensure compliance with applicable
|
|
||||||
rules and regulations.
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Links
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
|
||||||
<!-- INDEX -->
|
|
||||||
[report-bug]: #how-do-i-report-a-bug
|
|
||||||
[new-feature]: #how-can-i-request-a-new-feature
|
|
||||||
[commercial-support]: #does-ohif-offer-commercial-support
|
|
||||||
[academic]: #who-should-i-contact-about-academic-collaborations
|
|
||||||
[fda-clearance]: #does-the-ohif-viewer-have-510k-clearance-from-the-us-fda-or-ce-marking-from-the-european-commission
|
|
||||||
[hipaa]: #is-the-ohif-viewer-hipaa-compliant
|
|
||||||
<!-- OTHER -->
|
|
||||||
[501k-clearance]: https://www.fda.gov/MedicalDevices/DeviceRegulationandGuidance/HowtoMarketYourDevice/PremarketSubmissions/PremarketNotification510k/
|
|
||||||
[ce-marking]: https://ec.europa.eu/growth/single-market/ce-marking_en
|
|
||||||
[hipaa-def]: https://en.wikipedia.org/wiki/Health_Insurance_Portability_and_Accountability_Act
|
|
||||||
[new-issue]: https://github.com/OHIF/Viewers/issues/new/choose
|
|
||||||
[bug-report-template]: https://github.com/OHIF/Viewers/issues/new?assignees=&labels=Bug+Report+%3Abug%3A&template=---bug-report.md&title=
|
|
||||||
<!-- prettier-ignore-end -->
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# PWA vs Packaged
|
|
||||||
|
|
||||||
It's important to know that the OHIF Viewer project provides two different build
|
|
||||||
processes:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Static Asset output: For deploying PWAs
|
|
||||||
yarn run build
|
|
||||||
|
|
||||||
# Single `.js` script, for embedding viewer into existing apps
|
|
||||||
yarn run build:package
|
|
||||||
```
|
|
||||||
|
|
||||||
## Progressive Web Application (PWA)
|
|
||||||
|
|
||||||
> [Progressive Web Apps][pwa] are a new breed of web applications that meet the
|
|
||||||
> [following requirements][pwa-checklist]. Notably, targeting a PWA allows us
|
|
||||||
> provide a reliable, fast, and engaging experience across different devices and
|
|
||||||
> network conditions.
|
|
||||||
|
|
||||||
The OHIF Viewer is maintained as a [monorepo][monorepo]. We use WebPack to build
|
|
||||||
the many small static assets that comprise our application. Also generated is an
|
|
||||||
`index.html` that will serve as an entry point for loading configuration and the
|
|
||||||
application, as well as a `service-worker` that can intelligently cache files so
|
|
||||||
that subsequent requests are from the local file system instead of over the
|
|
||||||
network.
|
|
||||||
|
|
||||||
You can read more about this particular strategy in our
|
|
||||||
[Build for Production Deployment Guide](./../deployment/recipes/build-for-production.md)
|
|
||||||
|
|
||||||
## Commonjs Bundle (Packaged Script)
|
|
||||||
|
|
||||||
We are not supporting `Commonjs` bundling inside `OHIF-v3`.
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
# Scope of Project
|
|
||||||
|
|
||||||
The OHIF Viewer is a web based medical imaging viewer. This allows it to be used
|
|
||||||
on almost any device, anywhere. The OHIF Viewer is what is commonly reffered to
|
|
||||||
as a ["Dumb Client"][simplicable]
|
|
||||||
|
|
||||||
> A dumb client is software that fully depends on a connection to a server or
|
|
||||||
> cloud service for its functionality. Without a network connection, the
|
|
||||||
> software offers nothing useful. - [simplicable.com][simplicable]
|
|
||||||
|
|
||||||
While the Viewer persists some data, it's scope is limited to caching things
|
|
||||||
like user preferences and previous query paramaters. Because of this, the Viewer
|
|
||||||
has been built to be highly configurable to work with almost any web accessible
|
|
||||||
data source.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
To be more specific, the OHIF Viewer is a collection of HTML, JS, and CSS files.
|
|
||||||
These can be delivered to your end users however you would like:
|
|
||||||
|
|
||||||
- From the local networok
|
|
||||||
- From a remote web server
|
|
||||||
- From a CDN (content delivery network)
|
|
||||||
- From a service-worker's cache
|
|
||||||
- etc.
|
|
||||||
|
|
||||||
These "static asset" files are referred to collectively as a "Progressive Web
|
|
||||||
Application" (PWA), and have the same capabilities and limitations that all PWAs
|
|
||||||
have.
|
|
||||||
|
|
||||||
All studies, series, images, imageframes, metadata, and the images themselves
|
|
||||||
must come from an external source. There are many, many ways to provide this
|
|
||||||
information, the OHIF Viewer's scope **DOES NOT** encompass providing _any_
|
|
||||||
data; only the configuration necessary to interface with one or more of these
|
|
||||||
many data sources. The OHIF Viewer's scope **DOES** include configuration and
|
|
||||||
support for services that are protected with OpenID-Connect.
|
|
||||||
|
|
||||||
In an effort to aide our users and contributors, we attempt to provide several
|
|
||||||
[deployment and hosting recipes](./deployment/index.md) as potential starting
|
|
||||||
points. These are not meant to be rock solid, production ready, solutions; like
|
|
||||||
most recipes, they should be augmented to best fit you and your organization's
|
|
||||||
taste, preferences, etc.
|
|
||||||
|
|
||||||
## FAQ
|
|
||||||
|
|
||||||
_Am I able to cache studies for offline viewing?_
|
|
||||||
|
|
||||||
Not currently. A web page's offline cache capabilities are limited and somewhat
|
|
||||||
volatile (mostly imposed at the browser vendor level). For more robust offline
|
|
||||||
caching, you may want to consider a server on the local network, or packaging
|
|
||||||
the OHIF Viewer as a desktop application.
|
|
||||||
|
|
||||||
_Does the OHIF Viewer work with the local filesystem?_
|
|
||||||
|
|
||||||
It is possible to accomplish this through extensions; however, for an user
|
|
||||||
experience that accomodates a large number of studies, you would likely need to
|
|
||||||
package the OHIF Viewer as an [Electron app][electron].
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Links
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
|
||||||
[simplicable]: https://simplicable.com/new/dumb-client
|
|
||||||
[electron]: https://electronjs.org/
|
|
||||||
<!-- prettier-ignore-end -->
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# Help
|
|
||||||
|
|
||||||
We all need a little help sometimes. Don't let a few roadblocks stand in the way
|
|
||||||
of you building something awesome.
|
|
||||||
|
|
||||||
## Community Support
|
|
||||||
|
|
||||||
If you're a developer looking to contribute code, documentation, or discussion;
|
|
||||||
we are more than happy to help provide clarification and answer questions via
|
|
||||||
[GitHub issues][gh-issues] or our [community forum][google-group]. Regular
|
|
||||||
contributors may also be invited to join our Slack Group to streamline
|
|
||||||
discussion.
|
|
||||||
|
|
||||||
For bug reports and feature requests (including incomplete or confusing
|
|
||||||
documentation), [GitHub issues][gh-issues] continue to be your best avenue of
|
|
||||||
communication.
|
|
||||||
|
|
||||||
Complex issues specific to your organization/situation are still okay to post,
|
|
||||||
but they're less likely to receive a response. Unfortunately, we have limited
|
|
||||||
resources and must be judicious with how we allocate them. If you find yourself
|
|
||||||
in this situation and in need of assistance, it may be in your best interest to
|
|
||||||
persue paid support.
|
|
||||||
|
|
||||||
## Commercial Support
|
|
||||||
|
|
||||||
The Open Health Imaging Foundation does not offer commercial support, however,
|
|
||||||
some community members do offer consulting services:
|
|
||||||
|
|
||||||
- Rob Lewis ([Radical Imaging](http://radicalimaging.com/))
|
|
||||||
|
|
||||||
**Please file a Pull Request if you wish to add your name or organization to
|
|
||||||
this list.**
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Links
|
|
||||||
-->
|
|
||||||
|
|
||||||
[gh-issues]: https://github.com/OHIF/Viewers/issues/
|
|
||||||
[google-group]: https://groups.google.com/forum/#!forum/cornerstone-platform
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
# Our Process
|
|
||||||
|
|
||||||
Our process is a living, breathing thing. We strive to have regular
|
|
||||||
[retrospectives][retrospective] that help us shape and adapt our process to our
|
|
||||||
team's current needs. This document attempts to capture the broad strokes of
|
|
||||||
that process in an effort to:
|
|
||||||
|
|
||||||
- Strengthen community member involvement and understanding
|
|
||||||
- Welcome feedback and helpful suggestions
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
- [Issue Triage](#issue-triage)
|
|
||||||
- [Issue Curation ("backlog grooming")](#issue-curation-backlog-grooming)
|
|
||||||
- [Contributions (Pull Requests)](#contributions-pull-requests)
|
|
||||||
- [Releases](#releases)
|
|
||||||
|
|
||||||
|
|
||||||
## Issue Triage
|
|
||||||
|
|
||||||
[GitHub issues][gh-issues] are the best way to provide feedback, ask questions,
|
|
||||||
and suggest changes to the OHIF Viewer's core team. Community issues generally
|
|
||||||
fall into one of three categories, and are marked with a `triage` label when
|
|
||||||
created.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
| Issue Template Name | Description |
|
|
||||||
| ---------------------- | ---------------------------------------------------------------------------------------- |
|
|
||||||
| Community: Report 🐛 | Describe a new issue; Provide steps to reproduce; Expected versus actual result? |
|
|
||||||
| Community: Request ✋ | Describe a proposed new feature. Why should it be implemented? What is the impact/value? |
|
|
||||||
| Community: Question ❓ | Seek clarification or assistance relevant to the repository. |
|
|
||||||
|
|
||||||
_table 1. issue template names and descriptions_
|
|
||||||
|
|
||||||
Issues that require `triage` are akin to support tickets. As this is often our
|
|
||||||
first contact with would-be adopters and contributors, it's important that we
|
|
||||||
strive for timely responses and satisfactory resolutions. We attempt to
|
|
||||||
accomplish this by:
|
|
||||||
|
|
||||||
1. Responding to issues requiring `triage` at least once a week
|
|
||||||
2. Create new "official issues" from "community issues"
|
|
||||||
3. Provide clear guidance and next steps (when applicable)
|
|
||||||
4. Regularly clean up old (stale) issues
|
|
||||||
|
|
||||||
> 🖋 Less obviously, patterns in the issues being reported can highlight
|
|
||||||
> areas that need improvement. For example, users often have difficulty
|
|
||||||
> navigating CORS issues when deploying the OHIF Viewer -- how do we best reduce
|
|
||||||
> our ticket volume for this issue?
|
|
||||||
|
|
||||||
### Backlogged Issues
|
|
||||||
|
|
||||||
Community issues serve as vehicles of discussion that lead us to "backlogged
|
|
||||||
issues". Backlogged issues are the distilled and actionable information
|
|
||||||
extracted from community issues. They contain the scope and requirements
|
|
||||||
necessary for hand-off to a core-team (or community) contributor ^\_^
|
|
||||||
|
|
||||||
| Category | Description | Labels |
|
|
||||||
| -------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
||||||
| Bugs | An issue with steps that produce a bug (an unexpected result). | [Bug: Verified 🐛][label-bug] |
|
|
||||||
| Stories | A feature/enhancement with a clear benefit, boundaries, and requirements. | [Story 🙌][label-story] |
|
|
||||||
| Tasks | Changes that improve [UX], [DX], or test coverage; but don't impact application behavior | [Task: CI/Tooling 🤖][label-tooling], [Task: Docs 📖][label-docs], [Task: Refactor 🛠][label-refactor], [Task: Tests 🔬][label-tests] |
|
|
||||||
|
|
||||||
_table 2. backlogged issue types ([full list of labels][gh-labels])_
|
|
||||||
|
|
||||||
## Issue Curation (["backlog grooming"][groom-backlog])
|
|
||||||
|
|
||||||
If a [GitHub issue][gh-issues] has a `bug`, `story`, or `task` label; it's on
|
|
||||||
our backlog. If an issue is on our backlog, it means we are, at the very least,
|
|
||||||
committed to reviewing any community drafted Pull Requests to complete the
|
|
||||||
issue. If you're interested in seeing an issue completed but don't know where to
|
|
||||||
start, please don't hesitate to leave a comment!
|
|
||||||
|
|
||||||
While we don't yet have a long-term or quarterly road map, we do regularly add
|
|
||||||
items to our ["Active Development" GitHub Project Board][gh-board]. Items on
|
|
||||||
this project board are either in active development by Core Team members, or
|
|
||||||
queued up for development as in-progress items are completed.
|
|
||||||
|
|
||||||
> 🖋 Want to contribute but not sure where to start? Check out [Up for
|
|
||||||
> grabs][label-grabs] issues and our [Contributing
|
|
||||||
> documentation][contributing-docs]
|
|
||||||
|
|
||||||
## Contributions (Pull Requests)
|
|
||||||
|
|
||||||
Incoming Pull Requests (PRs) are triaged using the following labels. Code review
|
|
||||||
is performed on all PRs where the bug fix or added functionality is deemed
|
|
||||||
appropriate:
|
|
||||||
|
|
||||||
| Labels | Description |
|
|
||||||
| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
||||||
| **Classification** | |
|
|
||||||
| [PR: Bug Fix][label-bug] | Filed to address a Bug. |
|
|
||||||
| [PR: Draft][draft] | Filed to gather early feedback from the core team, but which is not intended for merging in the short term. |
|
|
||||||
| **Review Workflow** | |
|
|
||||||
| [PR: Awaiting Response 💬][awaiting-response] | The core team is waiting for additional information from the author. |
|
|
||||||
| [PR: Awaiting Review 👀][awaiting-review] | The core team has not yet performed a code review. |
|
|
||||||
| [PR: Awaiting Revisions 🖊][awaiting-revisions] | Following code review, this label is applied until the author has made sufficient changes. |
|
|
||||||
| **QA** | |
|
|
||||||
| [PR: Awaiting User Cases 💃][awaiting-stories] | The PR code changes need common language descriptions of impact to end users before the review can start |
|
|
||||||
| [PR: No UX Impact 🙃][no-ux-impact] | The PR code changes do not impact the user's experience |
|
|
||||||
|
|
||||||
We rely on GitHub Checks and integrations with third party services to evaluate
|
|
||||||
changes in code quality and test coverage. Tests must pass and User cases must
|
|
||||||
be present (when applicable) before a PR can be merged to master, and code
|
|
||||||
quality and test coverage must not changed by a significant margin. For some
|
|
||||||
repositories, visual screenshot-based tests are also included, and video
|
|
||||||
recordings of end-to-end tests are stored for later review.
|
|
||||||
|
|
||||||
[You can read more about our continous integration efforts here](/development/continous-integration.md)
|
|
||||||
|
|
||||||
## Releases
|
|
||||||
|
|
||||||
Releases are made automatically based on the type of commits which have been
|
|
||||||
merged (major.minor.patch). Releases are automatically pushed to NPM. Release
|
|
||||||
notes are automatically generated. Users can subscribe to GitHub and NPM
|
|
||||||
releases.
|
|
||||||
|
|
||||||
We host development, staging, and production environments for the Progressive
|
|
||||||
Web Application version of the OHIF Viewer. [Development][ohif-dev] always
|
|
||||||
reflects the latest changes on our master branch. [Staging][ohif-stage] is used
|
|
||||||
to regression test a release before a bi-weekly deploy to our [Production
|
|
||||||
environment][ohif-prod].
|
|
||||||
|
|
||||||
Important announcements are made on GitHub, tagged as Announcement, and pinned
|
|
||||||
so that they remain at the top of the Issue page.
|
|
||||||
|
|
||||||
The Core team occasionally performs full manual testing to begin the process of
|
|
||||||
releasing a Stable version. Once testing is complete, the known issues are
|
|
||||||
addressed and a Stable version is released.
|
|
||||||
|
|
||||||
<!--
|
|
||||||
LINKS
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
|
||||||
[groom-backlog]: https://www.agilealliance.org/glossary/backlog-grooming
|
|
||||||
[retrospective]: https://www.atlassian.com/team-playbook/plays/retrospective
|
|
||||||
[gh-issues]: https://github.com/OHIF/Viewers/issues/new/choose
|
|
||||||
[gh-labels]: https://github.com/OHIF/Viewers/labels
|
|
||||||
<!-- Issue Labels -->
|
|
||||||
[label-story]: https://github.com/OHIF/Viewers/labels/Story%20%3Araised_hands%3A
|
|
||||||
[label-tooling]: https://github.com/OHIF/Viewers/labels/Task%3A%20CI%2FTooling%20%3Arobot%3A
|
|
||||||
[label-docs]: https://github.com/OHIF/Viewers/labels/Task%3A%20Docs%20%3Abook%3A
|
|
||||||
[label-refactor]: https://github.com/OHIF/Viewers/labels/Task%3A%20Refactor%20%3Ahammer_and_wrench%3A
|
|
||||||
[label-tests]: https://github.com/OHIF/Viewers/labels/Task%3A%20Tests%20%3Amicroscope%3A
|
|
||||||
[label-bug]: https://github.com/OHIF/Viewers/labels/Bug%3A%20Verified%20%3Abug%3A
|
|
||||||
<!-- PR Labels -->
|
|
||||||
[draft]: https://github.com/OHIF/Viewers/labels/PR%3A%20Draft
|
|
||||||
[awaiting-response]: https://github.com/OHIF/Viewers/labels/PR%3A%20Awaiting%20Response%20%3Aspeech_balloon%3A
|
|
||||||
[awaiting-review]: https://github.com/OHIF/Viewers/labels/PR%3A%20Awaiting%20Review%20%3Aeyes%3A
|
|
||||||
[awaiting-stories]: https://github.com/OHIF/Viewers/labels/PR%3A%20Awaiting%20UX%20Stories%20%3Adancer%3A
|
|
||||||
[awaiting-revisions]: https://github.com/OHIF/Viewers/labels/PR%3A%20Awaiting%20Revisions%20%3Apen%3A
|
|
||||||
[no-ux-impact]: https://github.com/OHIF/Viewers/labels/PR%3A%20No%20UX%20Impact%20%3Aupside_down_face%3A
|
|
||||||
<!-- -->
|
|
||||||
[ohif-dev]: https://viewer-dev.ohif.org
|
|
||||||
[ohif-stage]: https://viewer-stage.ohif.org
|
|
||||||
[ohif-prod]: https://viewer.ohif.org
|
|
||||||
[gh-board]: https://github.com/OHIF/Viewers/projects/4
|
|
||||||
[label-grabs]: https://github.com/OHIF/Viewers/issues?q=is%3Aissue+is%3Aopen+label%3A%22Up+For+Grabs+%3Araising_hand_woman%3A%22
|
|
||||||
[contributing-docs]: ./development/contributing.md
|
|
||||||
<!-- prettier-ignore-end -->
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
# Services And Managers
|
|
||||||
|
|
||||||
- [Services And Managers](#services-and-managers)
|
|
||||||
- [Overview](#overview)
|
|
||||||
- [Services](#services)
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
Services are "concern-specific" code modules that can be consumed across layers. Services provide
|
|
||||||
a set of operations, often tied to some shared state, and are made available to
|
|
||||||
through out the app via the `ServicesManager`. Services are particularly well suited to
|
|
||||||
address [cross-cutting concerns][cross-cutting-concerns].
|
|
||||||
|
|
||||||
Each service should be:
|
|
||||||
|
|
||||||
- self-contained
|
|
||||||
- able to fail and/or be removed without breaking the application
|
|
||||||
- completely interchangeable with another module implementing the same interface
|
|
||||||
|
|
||||||
|
|
||||||
> In `OHIF-v3` we have added multiple non-UI services and have introduced **pub/sub** pattern to reduce coupling between layers.
|
|
||||||
>
|
|
||||||
> [Read more about Pub/Sub](./pubsub.md)
|
|
||||||
|
|
||||||
|
|
||||||
## Services
|
|
||||||
The following services is available in the `OHIF-v3`.
|
|
||||||
|
|
||||||
{% include "./_services.md" %}
|
|
||||||
|
|
||||||
|
|
||||||
<!--
|
|
||||||
LINKS
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
|
||||||
|
|
||||||
[core-services]: https://github.com/OHIF/Viewers/tree/master/platform/core/src/services
|
|
||||||
[services-manager]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/ServicesManager.js
|
|
||||||
[cross-cutting-concerns]: https://en.wikipedia.org/wiki/Cross-cutting_concern
|
|
||||||
<!-- prettier-ignore-end -->
|
|
||||||
@@ -1,315 +0,0 @@
|
|||||||
# UI Services
|
|
||||||
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
|
|
||||||
<!-- - Consistent look & feel
|
|
||||||
- Available in and “out” of React
|
|
||||||
- Supports ad hoc containers
|
|
||||||
- Exposes “setImplementation” method passed to Context Provider
|
|
||||||
- May set a default UI component
|
|
||||||
- API exposes get/set of Context Provider’s state -->
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
A typical web application will have components and state for common UI like
|
|
||||||
modals, notifications, dialogs, etc. A UI service makes it possible to leverage
|
|
||||||
these components from an extension.
|
|
||||||
|
|
||||||
We maintain the following UI Services:
|
|
||||||
|
|
||||||
- [UI Notification Service](ui-notification-service.md)
|
|
||||||
- [UI Modal Service](ui-modal-service.md)
|
|
||||||
- [UI Dialog Service](ui-dialog-service.md)
|
|
||||||
- [UI Viewport Dialog Service](ui-viewport-dialog-service.md)
|
|
||||||
- [Cine Service](cine.md)
|
|
||||||
- [Viewport Grid Service](ViewportGridService.md)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div style="text-align: center;">
|
|
||||||
<a href="/assets/img/ui-services.png">
|
|
||||||
<img src="/assets/img/ui-services.png" alt="UI Services Diagram" style="margin: 0 auto; max-width: 500px;" />
|
|
||||||
</a>
|
|
||||||
<div><i>Diagram showing relationship between React Context and UI Service</i></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- In `@ohif/core`, we have a collection of service factories. We select one we
|
|
||||||
would like our application to support, create an instance of it, and pass that
|
|
||||||
instance to our `ServicesManager` AND to a React component (in this example,
|
|
||||||
`ModalContext`'s provider).
|
|
||||||
|
|
||||||
The `ModalContext`'s provider:
|
|
||||||
|
|
||||||
- Exposes context values
|
|
||||||
- Exposes methods that leverage `useCallback` hooks
|
|
||||||
- Sets the service's implementation in a `useEffect` hook
|
|
||||||
|
|
||||||
The `ServicesManager` is:
|
|
||||||
|
|
||||||
- Passed to the `ExtensionManager`
|
|
||||||
- The `ExtensionManager` makes the `ServicesManager` available to:
|
|
||||||
- All of it's lifecycle hooks (`preRegistration`)
|
|
||||||
- Each "getModuleFunction" (`getToolbarModule`, `getPanelModule`, etc.) -->
|
|
||||||
|
|
||||||
|
|
||||||
## Providers for UI services
|
|
||||||
<mark>There are several context providers that wraps the application routes. This
|
|
||||||
makes the context values exposed in the app, and service's `setImplementation` can get run to override the implementation of the service.
|
|
||||||
|
|
||||||
```js
|
|
||||||
// platform/viewer/src/App.jsx
|
|
||||||
|
|
||||||
function App({ config, defaultExtensions }) {
|
|
||||||
/**...**/
|
|
||||||
/**...**/
|
|
||||||
return (
|
|
||||||
/**...**/
|
|
||||||
<ViewportGridProvider service={ViewportGridService}>
|
|
||||||
<ViewportDialogProvider service={UIViewportDialogService}>
|
|
||||||
<CineProvider service={CineService}>
|
|
||||||
<SnackbarProvider service={UINotificationService}>
|
|
||||||
<DialogProvider service={UIDialogService}>
|
|
||||||
<ModalProvider modal={Modal} service={UIModalService}>
|
|
||||||
{appRoutes}
|
|
||||||
</ModalProvider>
|
|
||||||
</DialogProvider>
|
|
||||||
</SnackbarProvider>
|
|
||||||
</CineProvider>
|
|
||||||
</ViewportDialogProvider>
|
|
||||||
</ViewportGridProvider>
|
|
||||||
/**...**/
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
For instance `UIModalService` has the following Public API:
|
|
||||||
|
|
||||||
```js
|
|
||||||
// platform/core/src/services/UIModalService/index.js
|
|
||||||
|
|
||||||
const publicAPI = {
|
|
||||||
name,
|
|
||||||
hide: _hide,
|
|
||||||
show: _show,
|
|
||||||
setServiceImplementation,
|
|
||||||
}
|
|
||||||
|
|
||||||
function setServiceImplementation({
|
|
||||||
hide: hideImplementation,
|
|
||||||
show: showImplementation,
|
|
||||||
}) {
|
|
||||||
/** ... **/
|
|
||||||
serviceImplementation._hide = hideImplementation
|
|
||||||
serviceImplementation._show = showImplementation
|
|
||||||
/** ... **/
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'UIModalService',
|
|
||||||
create: ({ configuration = {} }) => {
|
|
||||||
return publicAPI
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`UIModalService` implementation can be set (override) in its context provider. For instance in `ModalProvider` we have:
|
|
||||||
|
|
||||||
```js
|
|
||||||
// platform/ui/src/contextProviders/ModalProvider.jsx
|
|
||||||
|
|
||||||
import { Modal } from '@ohif/ui'
|
|
||||||
|
|
||||||
const ModalContext = createContext(null)
|
|
||||||
const { Provider } = ModalContext
|
|
||||||
|
|
||||||
export const useModal = () => useContext(ModalContext)
|
|
||||||
|
|
||||||
const ModalProvider = ({ children, modal: Modal, service }) => {
|
|
||||||
const DEFAULT_OPTIONS = {
|
|
||||||
content: null,
|
|
||||||
contentProps: null,
|
|
||||||
shouldCloseOnEsc: true,
|
|
||||||
isOpen: true,
|
|
||||||
closeButton: true,
|
|
||||||
title: null,
|
|
||||||
customClassName: '',
|
|
||||||
}
|
|
||||||
|
|
||||||
const show = useCallback(
|
|
||||||
(props) => setOptions({ ...options, ...props }),
|
|
||||||
[options]
|
|
||||||
)
|
|
||||||
|
|
||||||
const hide = useCallback(() => setOptions(DEFAULT_OPTIONS), [DEFAULT_OPTIONS])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (service) {
|
|
||||||
service.setServiceImplementation({ hide, show })
|
|
||||||
}
|
|
||||||
}, [hide, service, show])
|
|
||||||
|
|
||||||
const {
|
|
||||||
content: ModalContent,
|
|
||||||
contentProps,
|
|
||||||
isOpen,
|
|
||||||
title,
|
|
||||||
customClassName,
|
|
||||||
shouldCloseOnEsc,
|
|
||||||
closeButton,
|
|
||||||
} = options
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Provider value={{ show, hide }}>
|
|
||||||
{ModalContent && (
|
|
||||||
<Modal
|
|
||||||
className={classNames(customClassName, ModalContent.className)}
|
|
||||||
shouldCloseOnEsc={shouldCloseOnEsc}
|
|
||||||
isOpen={isOpen}
|
|
||||||
title={title}
|
|
||||||
closeButton={closeButton}
|
|
||||||
onClose={hide}
|
|
||||||
>
|
|
||||||
<ModalContent {...contentProps} show={show} hide={hide} />
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
{children}
|
|
||||||
</Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ModalProvider
|
|
||||||
|
|
||||||
export const ModalConsumer = ModalContext.Consumer
|
|
||||||
```
|
|
||||||
|
|
||||||
Therefore, anywhere in the app that we have access to react context we can use it by calling the `useModal` from `@ohif/ui`. As a matter of fact, we are utilizing the modal for the preference window which shows the hotkeys after clicking on the gear button on the right side of the header.
|
|
||||||
|
|
||||||
A `simplified` code for our worklist is:
|
|
||||||
|
|
||||||
|
|
||||||
```js
|
|
||||||
// platform/viewer/src/routes/WorkList/WorkList.jsx
|
|
||||||
|
|
||||||
import { useModal, Header } from '@ohif/ui'
|
|
||||||
|
|
||||||
function WorkList({
|
|
||||||
history,
|
|
||||||
data: studies,
|
|
||||||
dataTotal: studiesTotal,
|
|
||||||
isLoadingData,
|
|
||||||
dataSource,
|
|
||||||
hotkeysManager,
|
|
||||||
}) {
|
|
||||||
const { show, hide } = useModal()
|
|
||||||
|
|
||||||
/** ... **/
|
|
||||||
|
|
||||||
const menuOptions = [
|
|
||||||
{
|
|
||||||
title: t('Header:About'),
|
|
||||||
icon: 'info',
|
|
||||||
onClick: () => show({ content: AboutModal, title: 'About OHIF Viewer' }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('Header:Preferences'),
|
|
||||||
icon: 'settings',
|
|
||||||
onClick: () =>
|
|
||||||
show({
|
|
||||||
title: t('UserPreferencesModal:User Preferences'),
|
|
||||||
content: UserPreferences,
|
|
||||||
contentProps: {
|
|
||||||
hotkeyDefaults:
|
|
||||||
hotkeysManager.getValidHotkeyDefinitions(hotkeyDefaults),
|
|
||||||
hotkeyDefinitions,
|
|
||||||
onCancel: hide,
|
|
||||||
currentLanguage: currentLanguage(),
|
|
||||||
availableLanguages,
|
|
||||||
defaultLanguage,
|
|
||||||
onSubmit: (state) => {
|
|
||||||
i18n.changeLanguage(state.language.value)
|
|
||||||
hotkeysManager.setHotkeys(state.hotkeyDefinitions)
|
|
||||||
hide()
|
|
||||||
},
|
|
||||||
onReset: () => hotkeysManager.restoreDefaultBindings(),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
/** ... **/
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
/** ... **/
|
|
||||||
<Header isSticky menuOptions={menuOptions} isReturnEnabled={false} />
|
|
||||||
/** ... **/
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- ## Example -->
|
|
||||||
|
|
||||||
<!-- That's all fine and good, but it's still a little too abstract. What does this
|
|
||||||
translate to in practice?
|
|
||||||
|
|
||||||
```js
|
|
||||||
// In the application
|
|
||||||
import UINotificationService from '@ohif/core';
|
|
||||||
const servicesManager = new ServicesManager();
|
|
||||||
|
|
||||||
servicesManager.registerService(UINotificationService);
|
|
||||||
|
|
||||||
// UI Service Provider
|
|
||||||
useEffect(() => {
|
|
||||||
if (service) {
|
|
||||||
service.setServiceImplementation({ hide, show });
|
|
||||||
}
|
|
||||||
}, [service, hide, show]);
|
|
||||||
|
|
||||||
// In an extension
|
|
||||||
const { UINotificationService } = servicesManager.services;
|
|
||||||
|
|
||||||
if (UINotificationService) {
|
|
||||||
UINotificationService.show('Hello from the other side 👋');
|
|
||||||
}
|
|
||||||
``` -->
|
|
||||||
|
|
||||||
<!-- <div style="text-align: center;">
|
|
||||||
<a href="/assets/img/notification-example.gif">
|
|
||||||
<img src="/assets/img/notification-example.gif" alt="UI Notification Service Example" style="margin: 0 auto; max-width: 500px;" />
|
|
||||||
</a>
|
|
||||||
<div><i>GIF showing successful call of UINotificationService from an extension.</i></div>
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
## Tips & Tricks
|
|
||||||
|
|
||||||
It's important to remember that all we're doing is making it possible to control
|
|
||||||
bits of the application's UI from an extension. Here are a few non-obvious
|
|
||||||
takeaways worth mentioning:
|
|
||||||
|
|
||||||
- Your application code should continue to use React context
|
|
||||||
(consumers/providers) as it normally would
|
|
||||||
- You can substitute our "out of the box" UI implementations with your own
|
|
||||||
- You can create and register your own UI services
|
|
||||||
- You can choose not to register a service or provide a service implementation
|
|
||||||
- In extensions, you can provide fallback/alternative behavior if an expected
|
|
||||||
service is not registered
|
|
||||||
- No `UIModalService`? Use the `UINotificationService` to notify users.
|
|
||||||
- You can technically register a service in an extension and expose it to the core application
|
|
||||||
|
|
||||||
> Note: These are recommended patterns, not hard and fast rules. Following them
|
|
||||||
> will help reduce confusion and interoperability with the larger OHIF
|
|
||||||
> community, but they're not silver bullets. Please speak up, create an issue,
|
|
||||||
> if you would like to discuss new services or improvements to this pattern.
|
|
||||||
|
|
||||||
<!-- ## Maintained Services
|
|
||||||
|
|
||||||
Our `@ohif/viewer` project is an example of how to glue together the different
|
|
||||||
parts and pieces of the OHIF Platform to create a polished and powerful product.
|
|
||||||
To accomplish that, we maintain several UI Services that you can use in your own
|
|
||||||
project, or provide alternative implementations for: -->
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
# UI Dialog Service
|
|
||||||
|
|
||||||
Dialogs have similar characteristics to that of Modals, but often with a
|
|
||||||
streamlined focus. They can be helpful when:
|
|
||||||
|
|
||||||
- We need to grab the user's attention
|
|
||||||
- We need user input
|
|
||||||
- We need to show additional information
|
|
||||||
|
|
||||||
If you're curious about the DOs and DON'Ts of dialogs and modals, check out this
|
|
||||||
article: ["Best Practices for Modals / Overlays / Dialog Windows"][ux-article]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Interface
|
|
||||||
|
|
||||||
For a more detailed look on the options and return values each of these methods
|
|
||||||
is expected to support, [check out it's interface in `@ohif/core`][interface]
|
|
||||||
|
|
||||||
| API Member | Description |
|
|
||||||
| -------------- | ------------------------------------------------------ |
|
|
||||||
| `create()` | Creates a new Dialog that is displayed until dismissed |
|
|
||||||
| `dismiss()` | Dismisses the specified dialog |
|
|
||||||
| `dismissAll()` | Dismisses all dialogs |
|
|
||||||
|
|
||||||
## Implementations
|
|
||||||
|
|
||||||
| Implementation | Consumer |
|
|
||||||
| ------------------------------------ | -------------------------- |
|
|
||||||
| [Dialog Provider][dialog-provider]\* | Baked into Dialog Provider |
|
|
||||||
|
|
||||||
`*` - Denotes maintained by OHIF
|
|
||||||
|
|
||||||
> 3rd Party implementers may be added to this table via pull requests.
|
|
||||||
|
|
||||||
<!--
|
|
||||||
LINKS
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
|
||||||
[interface]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/UIDialogService/index.js
|
|
||||||
[dialog-provider]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/contextProviders/DialogProvider.js
|
|
||||||
[ux-article]: https://uxplanet.org/best-practices-for-modals-overlays-dialog-windows-c00c66cddd8c
|
|
||||||
<!-- prettier-ignore-end -->
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
# UI Modal Service
|
|
||||||
|
|
||||||
Modals have similar characteristics to that of Dialogs, but are often larger,
|
|
||||||
and only allow for a single instance to be viewable at once. They also tend to
|
|
||||||
be centered, and not draggable. They're commonly used when:
|
|
||||||
|
|
||||||
- We need to grab the user's attention
|
|
||||||
- We need user input
|
|
||||||
- We need to show additional information
|
|
||||||
|
|
||||||
If you're curious about the DOs and DON'Ts of dialogs and modals, check out this
|
|
||||||
article: ["Best Practices for Modals / Overlays / Dialog Windows"][ux-article]
|
|
||||||
|
|
||||||
<div style="padding:56.25% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/549261451?badge=0&autopause=0&player_id=0&app_id=58479" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen style="position:absolute;top:0;left:0;width:100%;height:100%;" title="Viewer Modal"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
|
|
||||||
|
|
||||||
## Interface
|
|
||||||
|
|
||||||
For a more detailed look on the options and return values each of these methods
|
|
||||||
is expected to support, [check out it's interface in `@ohif/core`][interface]
|
|
||||||
|
|
||||||
| API Member | Description |
|
|
||||||
| ---------- | ------------------------------------- |
|
|
||||||
| `hide()` | Hides the open modal |
|
|
||||||
| `show()` | Shows the provided content in a modal |
|
|
||||||
|
|
||||||
## Implementations
|
|
||||||
|
|
||||||
| Implementation | Consumer |
|
|
||||||
| ---------------------------------- | --------- |
|
|
||||||
| [Modal Provider][modal-provider]\* | Modal.jsx |
|
|
||||||
|
|
||||||
`*` - Denotes maintained by OHIF
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
> 3rd Party implementers may be added to this table via pull requests.
|
|
||||||
|
|
||||||
<!--
|
|
||||||
LINKS
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
|
||||||
[interface]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/UIModalService/index.js
|
|
||||||
[modal-provider]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/contextProviders/ModalProvider.js
|
|
||||||
[modal-consumer]: https://github.com/OHIF/Viewers/tree/master/platform/ui/src/components/ohifModal
|
|
||||||
[ux-article]: https://uxplanet.org/best-practices-for-modals-overlays-dialog-windows-c00c66cddd8c
|
|
||||||
<!-- prettier-ignore-end -->
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
# UI Notification Service
|
|
||||||
|
|
||||||
Notifications can be annoying and disruptive. They can also deliver timely
|
|
||||||
helpful information, or expedite the user's workflow. Here is some high level
|
|
||||||
guidance on when and how to use them:
|
|
||||||
|
|
||||||
- Notifications should be non-interfering (timely, relevant, important)
|
|
||||||
- We should only show small/brief notifications
|
|
||||||
- Notifications should be contextual to current behavior/actions
|
|
||||||
- Notifications can serve warnings (acting as a confirmation)
|
|
||||||
|
|
||||||
If you're curious about the DOs and DON'Ts of notifications, check out this
|
|
||||||
article: ["How To Design Notifications For Better UX"][ux-article]
|
|
||||||
|
|
||||||
<div style="padding:56.25% 0 0 0;position:relative;"><iframe src="https://player.vimeo.com/video/549260975?badge=0&autopause=0&player_id=0&app_id=58479" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen style="position:absolute;top:0;left:0;width:100%;height:100%;" title="notification"></iframe></div><script src="https://player.vimeo.com/api/player.js"></script>
|
|
||||||
## Interface
|
|
||||||
|
|
||||||
For a more detailed look on the options and return values each of these methods
|
|
||||||
is expected to support, [check out it's interface in `@ohif/core`][interface]
|
|
||||||
|
|
||||||
| API Member | Description |
|
|
||||||
| ---------- | --------------------------------------- |
|
|
||||||
| `hide()` | Hides the specified notification |
|
|
||||||
| `show()` | Creates and displays a new notification |
|
|
||||||
|
|
||||||
## Implementations
|
|
||||||
|
|
||||||
| Implementation | Consumer |
|
|
||||||
| ---------------------------------------- | ----------------------------------------- |
|
|
||||||
| [Snackbar Provider][snackbar-provider]\* | [SnackbarContainer][snackbar-container]\* |
|
|
||||||
|
|
||||||
`*` - Denotes maintained by OHIF
|
|
||||||
|
|
||||||
> 3rd Party implementers may be added to this table via pull requests.
|
|
||||||
|
|
||||||
<!--
|
|
||||||
LINKS
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
|
||||||
[interface]: https://github.com/OHIF/Viewers/blob/master/platform/core/src/services/UINotificationService/index.js
|
|
||||||
[snackbar-provider]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/contextProviders/SnackbarProvider.js
|
|
||||||
[snackbar-container]: https://github.com/OHIF/Viewers/blob/master/platform/ui/src/components/snackbar/SnackbarContainer.js
|
|
||||||
[ux-article]: https://uxplanet.org/how-to-design-notifications-for-better-ux-6fb0711be54d
|
|
||||||
<!-- prettier-ignore-end -->
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
# Viewer: Configuration
|
|
||||||
|
|
||||||
We maintain a number of common viewer application configurations at
|
|
||||||
[`<root>/platform/viewer/public/configs`][config-dir].
|
|
||||||
|
|
||||||
You can take a look at how to use different configs in the [Environment Variables](../configuring/index.md#environment-variables)
|
|
||||||
|
|
||||||
|
|
||||||
```js
|
|
||||||
window.config = {
|
|
||||||
routerBasename: '/',
|
|
||||||
/**
|
|
||||||
* "White Labeling" is used to change the branding, look, and feel of the OHIF
|
|
||||||
* Viewer. These settings, and the color variables that are used by our components,
|
|
||||||
* are the easiest way to rebrand the application.
|
|
||||||
*
|
|
||||||
* More extensive changes are made possible through swapping out the UI library,
|
|
||||||
* Viewer project, or extensions.
|
|
||||||
*/
|
|
||||||
whiteLabeling: {
|
|
||||||
/** ... **/
|
|
||||||
},
|
|
||||||
httpErrorHandler: {
|
|
||||||
/** coming soon **/
|
|
||||||
},
|
|
||||||
extensions: [],
|
|
||||||
showStudyList: true,
|
|
||||||
filterQueryParam: false,
|
|
||||||
dataSources: [
|
|
||||||
{
|
|
||||||
friendlyName: 'dcmjs DICOMWeb Server',
|
|
||||||
namespace: 'org.ohif.default.dataSourcesModule.dicomweb',
|
|
||||||
sourceName: 'dicomweb',
|
|
||||||
configuration: {
|
|
||||||
name: 'DCM4CHEE',
|
|
||||||
wadoUriRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/wado',
|
|
||||||
qidoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
|
|
||||||
wadoRoot: 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs',
|
|
||||||
qidoSupportsIncludeField: true,
|
|
||||||
supportsReject: true,
|
|
||||||
imageRendering: 'wadors',
|
|
||||||
thumbnailRendering: 'wadors',
|
|
||||||
enableStudyLazyLoad: true,
|
|
||||||
supportsFuzzyMatching: true,
|
|
||||||
supportsWildcard: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
<!--
|
|
||||||
LINKS
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
|
||||||
[config-dir]: https://github.com/OHIF/Viewers/tree/master/platform/viewer/public/config
|
|
||||||
<!-- prettier-ignore-end -->
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Environment Variables
|
|
||||||
|
|
||||||
There are a number of environment variables we use at build time to influence the output application's behavior.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Application
|
|
||||||
NODE_ENV=< production | development >
|
|
||||||
DEBUG=< true | false >
|
|
||||||
APP_CONFIG=< relative path to application configuration file >
|
|
||||||
PUBLIC_URL=<>
|
|
||||||
VERSION_NUMBER=<Set by CircleCI>
|
|
||||||
BUILD_NUM=<Set by CircleCI>
|
|
||||||
# i18n
|
|
||||||
USE_LOCIZE=<false>
|
|
||||||
LOCIZE_PROJECTID=<ProjectID to pull translations for>
|
|
||||||
LOCIZE_API_KEY=<To enable Locize live editing of translations>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Setting Environment Variables
|
|
||||||
|
|
||||||
- `npx cross-env`
|
|
||||||
- `.env` files
|
|
||||||
- env variables on build machine, or for terminal session
|
|
||||||
@@ -1,332 +0,0 @@
|
|||||||
# Viewer: Internationalization
|
|
||||||
|
|
||||||
OHIF supports internationalization using [i18next](https://www.i18next.com/)
|
|
||||||
through the npm package [@ohif/i18n](https://www.npmjs.com/package/@ohif/i18n),
|
|
||||||
where is the main instance of i18n containing several languages and tools.
|
|
||||||
|
|
||||||
<div class='row'>
|
|
||||||
<div class='column'>
|
|
||||||
<p>Our translation management is powered by <a href="https://locize.com/" target="_blank" rel="noopener noreferrer">Locize</a> through their generous support of open source.</p>
|
|
||||||
</div>
|
|
||||||
<div class='column'>
|
|
||||||
<a href="https://locize.com/" target="_blank" rel="noopener noreferrer" style='padding: 20px'>
|
|
||||||
<img src="../assets/img/locizeSponsor.svg" alt="Locize Translation Management Logo">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
## How to change language for the viewer?
|
|
||||||
You can take a look into user manuals to see how to change the viewer's language.
|
|
||||||
In summary you can change the language:
|
|
||||||
|
|
||||||
- In the preference modals
|
|
||||||
- Using the language query in the URL: `lng=Test-LNG`
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Installing
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn add @ohif/i18n
|
|
||||||
|
|
||||||
# OR
|
|
||||||
|
|
||||||
npm install --save @ohif/i18n
|
|
||||||
```
|
|
||||||
|
|
||||||
## How it works
|
|
||||||
|
|
||||||
After installing `@ohif/i18n` npm package, the translation function
|
|
||||||
[t](https://www.i18next.com/overview/api#t) can be used [with](#with-react) or
|
|
||||||
[without](#without-react) React.
|
|
||||||
|
|
||||||
A translation will occur every time a text match happens in a
|
|
||||||
[t](https://www.i18next.com/overview/api#t) function.
|
|
||||||
|
|
||||||
The [t](https://www.i18next.com/overview/api#t) function is responsible for
|
|
||||||
getting translations using all the power of i18next.
|
|
||||||
|
|
||||||
E.g.
|
|
||||||
|
|
||||||
Before:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div>my translated text</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
After:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div>{t('my translated text')}</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
If the translation.json file contains a key that matches the HTML content e.g.
|
|
||||||
`my translated text`, it will be replaced automatically by the
|
|
||||||
[t](https://www.i18next.com/overview/api#t) function.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### With React
|
|
||||||
|
|
||||||
This section will introduce you to [react-i18next](https://react.i18next.com/)
|
|
||||||
basics and show how to implement the [t](https://www.i18next.com/overview/api#t)
|
|
||||||
function easily.
|
|
||||||
|
|
||||||
|
|
||||||
#### Using Hooks
|
|
||||||
You can use `useTranslation` hooks that is provided by `react-i18next`
|
|
||||||
|
|
||||||
|
|
||||||
You can read more about this [here](https://react.i18next.com/latest/usetranslation-hook).
|
|
||||||
|
|
||||||
|
|
||||||
```js
|
|
||||||
import React from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
|
|
||||||
function MyComponent() {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return <p>{t('my translated text')}</p>;
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using outside of OHIF viewer
|
|
||||||
|
|
||||||
OHIF Viewer already sets a main
|
|
||||||
[I18nextProvider](https://react.i18next.com/latest/i18nextprovider) connected to
|
|
||||||
the shared i18n instance from `@ohif/i18n`, all extensions inside OHIF Viewer
|
|
||||||
will share this same provider at the end, you don't need to set new providers at
|
|
||||||
all.
|
|
||||||
|
|
||||||
But, if you need to use it completely outside of OHIF viewer, you can set the
|
|
||||||
I18nextProvider this way:
|
|
||||||
|
|
||||||
```js
|
|
||||||
import i18n from '@ohif/i18n';
|
|
||||||
import { I18nextProvider } from 'react-i18next';
|
|
||||||
import App from './App';
|
|
||||||
|
|
||||||
<I18nextProvider i18n={i18n}>
|
|
||||||
<App />
|
|
||||||
</I18nextProvider>;
|
|
||||||
```
|
|
||||||
|
|
||||||
After setting `I18nextProvider` in your React App, all translations from
|
|
||||||
`@ohif/i18n` should be available following the basic [With React](#with-react)
|
|
||||||
usage.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Without React
|
|
||||||
|
|
||||||
When needed, you can also use available translations _without React_.
|
|
||||||
|
|
||||||
E.g.
|
|
||||||
|
|
||||||
```js
|
|
||||||
import { T } from '@ohif/i18n';
|
|
||||||
console.log(T('my translated text'));
|
|
||||||
console.log(T('$t(Common:Play) my translated text'));
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Main Concepts While Translating
|
|
||||||
|
|
||||||
## Namespaces
|
|
||||||
|
|
||||||
Namespaces are being used to organize translations in smaller portions, combined
|
|
||||||
semantically or by use. Each `.json` file inside `@ohif/i18n` npm package
|
|
||||||
becomes a new namespace automatically.
|
|
||||||
|
|
||||||
- Buttons: All buttons translations
|
|
||||||
- CineDialog: Translations for the toll tips inside the Cine Player Dialog
|
|
||||||
- Common: all common jargons that can be reused like `t('$t(common:image)')`
|
|
||||||
- Header: translations related to OHIF's Header Top Bar
|
|
||||||
- MeasurementTable - Translations for the `@ohif/ui` Measurement Table
|
|
||||||
- UserPreferencesModal - Translations for the `@ohif/ui` Preferences Modal
|
|
||||||
- Modals - Translations available for other modals
|
|
||||||
- PatientInfo - Translations for patients info hover
|
|
||||||
- SidePanel - Translations for side panels
|
|
||||||
- ToolTip - Translations for tool tips
|
|
||||||
|
|
||||||
### How to use another NameSpace inside the current NameSpace?
|
|
||||||
|
|
||||||
i18next provides a parsing feature able to get translations strings from any
|
|
||||||
NameSpace, like this following example getting data from `Common` NameSpace:
|
|
||||||
|
|
||||||
```
|
|
||||||
$t('Common:Reset')
|
|
||||||
```
|
|
||||||
|
|
||||||
## Extending Languages in @ohif/i18n
|
|
||||||
|
|
||||||
Sometimes, even using the same language, some nouns or jargons can change
|
|
||||||
according to the country, states or even from Hospital to Hospital.
|
|
||||||
|
|
||||||
In this cases, you don't need to set an entire language again, you can extend
|
|
||||||
languages creating a new folder inside a pre existent language folder and
|
|
||||||
@ohif/i18n will do the hard work.
|
|
||||||
|
|
||||||
This new folder must to be called with a double character name, like the `UK` in
|
|
||||||
the following file tree:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
|-- src
|
|
||||||
|-- locales
|
|
||||||
index.js
|
|
||||||
|-- en
|
|
||||||
|-- Buttons.json
|
|
||||||
index.js
|
|
||||||
| UK
|
|
||||||
|-- Buttons.js
|
|
||||||
indes.js
|
|
||||||
| US
|
|
||||||
|-- Buttons.js
|
|
||||||
index.js
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
All properties inside a Namespace will be merged in the new sub language, e.g
|
|
||||||
`en-US` and `en-UK` will merge the props with `en`, using i18next's fallback
|
|
||||||
languages tool.
|
|
||||||
|
|
||||||
You will need to export all Json files in your `index.js` file, mounting an
|
|
||||||
object like this:
|
|
||||||
|
|
||||||
```js
|
|
||||||
{
|
|
||||||
en: {
|
|
||||||
NameSpace: {
|
|
||||||
keyWord1: 'keyWord1Translation',
|
|
||||||
keyWord2: 'keyWord2Translation',
|
|
||||||
keyWord3: 'keyWord3Translation',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'en-UK': {
|
|
||||||
NameSpace: {
|
|
||||||
keyWord1: 'keyWord1DifferentTranslation',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Please check the `index.js` files inside locales folder for an example of this
|
|
||||||
exporting structure.
|
|
||||||
|
|
||||||
### Extending languages dynamically
|
|
||||||
|
|
||||||
You have access to the i18next instance, so you can use the
|
|
||||||
[addResourceBundle](https://www.i18next.com/how-to/add-or-load-translations#add-after-init)
|
|
||||||
method to add and change language resources as needed.
|
|
||||||
|
|
||||||
E.g.
|
|
||||||
|
|
||||||
```js
|
|
||||||
import { i18n } from '@ohif/i18n';
|
|
||||||
i18next.addResourceBundle('pt-BR', 'Buttons', {
|
|
||||||
Angle: 'Ângulo',
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### How to set a whole new language
|
|
||||||
|
|
||||||
To set a brand new language you can do it in two different ways:
|
|
||||||
|
|
||||||
- Opening a pull request for `@ohif/i18n` and sharing the translation with the
|
|
||||||
community. 😍 Please see [Contributing](#contributing-with-new-languages)
|
|
||||||
section for further information.
|
|
||||||
|
|
||||||
- Setting it only in your project or extension:
|
|
||||||
|
|
||||||
You'll need a final object like the following, what is setting French as
|
|
||||||
language, and send it to `addLocales` method.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const newLanguage =
|
|
||||||
{
|
|
||||||
fr: {
|
|
||||||
Commons: {
|
|
||||||
"Reset": "Réinitialiser",
|
|
||||||
"Previous": "Précédent",
|
|
||||||
},
|
|
||||||
Buttons: {
|
|
||||||
"Rectangle": "Rectangle",
|
|
||||||
"Circle": "Cercle",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
To make it easier to translate, you can copy the .json files in the /locales
|
|
||||||
folder and theirs index.js exporters, keeping same keys and NameSpaces.
|
|
||||||
Importing the main index.js file, will provide you an Object as expected by the
|
|
||||||
method `addlocales`;
|
|
||||||
|
|
||||||
E.g. of `addLocales` usage
|
|
||||||
|
|
||||||
```js
|
|
||||||
import { addLocales } from '@ohif/i18n';
|
|
||||||
import locales from './locales/index.js';
|
|
||||||
addLocales(locales);
|
|
||||||
```
|
|
||||||
|
|
||||||
You can also set them manually, one by one, using this
|
|
||||||
[method](#extending-languages-dynamically).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Test Language
|
|
||||||
We have created a test language that its translations can be seen in the locales folder. You can copy paste the folder and its `.json` namespaces and add your custom
|
|
||||||
language translations.
|
|
||||||
|
|
||||||
> If you apply the test-LNG you can see all the elements get appended with 'Test {}'.
|
|
||||||
> For instance `Study list` becomes `Test Study list`.
|
|
||||||
|
|
||||||
## Language Detections
|
|
||||||
|
|
||||||
@ohif/i18n uses
|
|
||||||
[i18next-browser-languageDetector](https://github.com/i18next/i18next-browser-languageDetector)
|
|
||||||
to manage detections, also exports a method called initI18n that accepts a new
|
|
||||||
detector config as parameter.
|
|
||||||
|
|
||||||
### Changing the language
|
|
||||||
|
|
||||||
OHIF Viewer accepts a query param called `lng` in the url to change the
|
|
||||||
language.
|
|
||||||
|
|
||||||
E.g.
|
|
||||||
|
|
||||||
```
|
|
||||||
https://docs.ohif.org/demo/?lng=es-MX
|
|
||||||
```
|
|
||||||
|
|
||||||
### Language Persistence
|
|
||||||
|
|
||||||
The user's language preference is kept automatically by the detector and stored
|
|
||||||
at a cookie called 'i18next', and in a localstorage key called 'i18nextLng'.
|
|
||||||
These names can be changed with a new
|
|
||||||
[Detector Config](https://github.com/i18next/i18next-browser-languageDetector).
|
|
||||||
|
|
||||||
## Debugging translations
|
|
||||||
|
|
||||||
There is an environment variable responsible for debugging the translations,
|
|
||||||
called `REACT_APP_I18N_DEBUG`.
|
|
||||||
|
|
||||||
Run the project as following to get full debug information:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
REACT_APP_I18N_DEBUG=true yarn run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Contributing with new languages
|
|
||||||
|
|
||||||
Contributions of any kind are welcome! Please check the
|
|
||||||
[instructions](../development/contributing.md).
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
# Viewer: Theming
|
|
||||||
|
|
||||||
|
|
||||||
`OHIF-v3` has introduced the [`LayoutTemplateModule`](../extensions/modules/layout-template.md) which enables addition of custom layouts. You can easily design your custom components inside an extension and consume it via the layoutTemplate module you write.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Tailwind CSS
|
|
||||||
[Tailwind CSS](https://tailwindcss.com/) is a utility-first CSS framework for creating custom user interfaces.
|
|
||||||
|
|
||||||
|
|
||||||
Below you can see a compiled version of the tailwind configs.
|
|
||||||
Each section can be edited accordingly. For instance screen size break points, primary
|
|
||||||
and secondary colors, etc.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
```js
|
|
||||||
module.exports = {
|
|
||||||
prefix: '',
|
|
||||||
important: false,
|
|
||||||
separator: ':',
|
|
||||||
theme: {
|
|
||||||
screens: {
|
|
||||||
sm: '640px',
|
|
||||||
md: '768px',
|
|
||||||
lg: '1024px',
|
|
||||||
xl: '1280px',
|
|
||||||
},
|
|
||||||
colors: {
|
|
||||||
overlay: 'rgba(0, 0, 0, 0.8)',
|
|
||||||
transparent: 'transparent',
|
|
||||||
black: '#000',
|
|
||||||
white: '#fff',
|
|
||||||
initial: 'initial',
|
|
||||||
inherit: 'inherit',
|
|
||||||
|
|
||||||
indigo: {
|
|
||||||
dark: '#0b1a42',
|
|
||||||
},
|
|
||||||
aqua: {
|
|
||||||
pale: '#7bb2ce',
|
|
||||||
},
|
|
||||||
|
|
||||||
primary: {
|
|
||||||
light: '#5acce6',
|
|
||||||
main: '#0944b3',
|
|
||||||
dark: '#090c29',
|
|
||||||
active: '#348cfd',
|
|
||||||
},
|
|
||||||
|
|
||||||
secondary: {
|
|
||||||
light: '#3a3f99',
|
|
||||||
main: '#2b166b',
|
|
||||||
dark: '#041c4a',
|
|
||||||
active: '#1f1f27',
|
|
||||||
},
|
|
||||||
|
|
||||||
common: {
|
|
||||||
bright: '#e1e1e1',
|
|
||||||
light: '#a19fad',
|
|
||||||
main: '#fff',
|
|
||||||
dark: '#726f7e',
|
|
||||||
active: '#2c3074',
|
|
||||||
},
|
|
||||||
|
|
||||||
customgreen: {
|
|
||||||
100: '#05D97C',
|
|
||||||
},
|
|
||||||
|
|
||||||
customblue: {
|
|
||||||
100: '#c4fdff',
|
|
||||||
200: '#38daff',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
You can also use the color variable like before. For instance:
|
|
||||||
|
|
||||||
|
|
||||||
```js
|
|
||||||
primary: {
|
|
||||||
default: ‘var(--default-color)‘,
|
|
||||||
light: ‘#5ACCE6’,
|
|
||||||
main: ‘#0944B3’,
|
|
||||||
dark: ‘#090C29’,
|
|
||||||
active: ‘#348CFD’,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## White Labeling
|
|
||||||
|
|
||||||
A white-label product is a product or service produced by one company (the producer) that other companies (the marketers) rebrand to make it appear as if they had made it - [Wikipedia: White-Label Product](https://en.wikipedia.org/wiki/White-label_product)
|
|
||||||
|
|
||||||
Current white-labeling options are limited.
|
|
||||||
We expose the ability to replace the "Logo" section of the application with a custom "Logo" component. You can do this by adding a whiteLabeling key to your configuration file.
|
|
||||||
|
|
||||||
```js
|
|
||||||
window.config = {
|
|
||||||
/** .. **/
|
|
||||||
whiteLabeling: {
|
|
||||||
createLogoComponentFn: function (React) {
|
|
||||||
return React.createElement(
|
|
||||||
'a',
|
|
||||||
{
|
|
||||||
target: '_blank',
|
|
||||||
rel: 'noopener noreferrer',
|
|
||||||
className: 'text-white underline',
|
|
||||||
href: 'http://radicalimaging.com',
|
|
||||||
},
|
|
||||||
React.createElement('h5', {}, 'RADICAL IMAGING')
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
/** .. **/
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
> You can simply use the stylings from tailwind CSS in the whiteLabeling
|
|
||||||
|
|
||||||
|
|
||||||
In addition to text, you can also add your custom logo
|
|
||||||
|
|
||||||
|
|
||||||
```js
|
|
||||||
window.config = {
|
|
||||||
/** .. **/
|
|
||||||
whiteLabeling: {
|
|
||||||
createLogoComponentFn: function (React) {
|
|
||||||
return React.createElement(
|
|
||||||
'a',
|
|
||||||
{
|
|
||||||
target: '_self',
|
|
||||||
rel: 'noopener noreferrer',
|
|
||||||
className: 'text-purple-600 line-through',
|
|
||||||
href: '/',
|
|
||||||
},
|
|
||||||
React.createElement('img', {
|
|
||||||
src: './customLogo.svg',
|
|
||||||
// className: 'w-8 h-8',
|
|
||||||
})
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
/** .. **/
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The output will look like
|
|
||||||
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Links
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
|
||||||
[wikipedia]: https://en.wikipedia.org/wiki/White-label_product
|
|
||||||
<!-- prettier-ignore-end -->
|
|
||||||
@@ -1,11 +1,4 @@
|
|||||||
---
|
<div class='row'>
|
||||||
id: Introduction
|
|
||||||
slug: /
|
|
||||||
sidebar_position: 1
|
|
||||||
---
|
|
||||||
|
|
||||||
|
|
||||||
<!-- <div class='row'>
|
|
||||||
<div class='column' style='text-align: right; padding: 0 20px'>
|
<div class='column' style='text-align: right; padding: 0 20px'>
|
||||||
<strong>Looking for a Live Demo?</strong>
|
<strong>Looking for a Live Demo?</strong>
|
||||||
<a href="http://viewer.ohif.org/">Preview The OHIF Viewer</a>
|
<a href="http://viewer.ohif.org/">Preview The OHIF Viewer</a>
|
||||||
@@ -15,40 +8,59 @@ sidebar_position: 1
|
|||||||
<img src="https://www.netlify.com/img/global/badges/netlify-color-bg.svg"/>
|
<img src="https://www.netlify.com/img/global/badges/netlify-color-bg.svg"/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div> -->
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<strong>Looking for a Live Demo?</strong>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
> ATTENTION! You are looking at the docs for the `OHIF-v3` Viewer (third time is always a charm). If you're looking for the our `OHIF-v2` (React stable version) or OHIF-v1 (deprecated `Meteor` version) select it's version from the dropdown box in the top left corner
|
||||||
<!-- <a href="https://www.netlify.com"> -->
|
|
||||||
|
|
||||||
<div className="text--center">
|
|
||||||
<a style={{marginRight:'10px'}} href="http://viewer.ohif.org/">Preview The OHIF Viewer</a>
|
|
||||||
<a href="https://www.netlify.com"/>
|
|
||||||
<img style={{width:'70px'}} src="https://www.netlify.com/img/global/badges/netlify-color-bg.svg" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
<img className={{width:'50px'}} src="https://www.netlify.com/img/global/badges/netlify-color-bg.svg"/>
|
|
||||||
</a> -->
|
|
||||||
|
|
||||||
|
|
||||||
> ATTENTION! You are looking at the docs for the `React` version of the OHIF
|
|
||||||
> Viewer. If you're looking for the `Meteor` version's documentation (now
|
|
||||||
> deprecated), select it's version from the dropdown box in the top right corner
|
|
||||||
> of this page.
|
> of this page.
|
||||||
|
|
||||||
|
|
||||||
|
# Introduction
|
||||||
|
|
||||||
The [Open Health Imaging Foundation][ohif-org] (OHIF) Viewer is an open source,
|
The [Open Health Imaging Foundation][ohif-org] (OHIF) Viewer is an open source,
|
||||||
web-based, medical imaging viewer. It can be configured to connect to Image
|
web-based, medical imaging viewer. It can be configured to connect to Image
|
||||||
Archives that support [DicomWeb][dicom-web], and offers support for mapping to
|
Archives that support [DicomWeb][dicom-web], and offers support for mapping to
|
||||||
proprietary API formats. OHIF maintained extensions add support for viewing,
|
proprietary API formats. OHIF maintained extensions add support for viewing,
|
||||||
annotating, and reporting on DICOM images in 2D (slices) and 3D (volumes).
|
annotating, and reporting on DICOM images in 2D (slices) and 3D (volumes).
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
<center>The <strong>OHIF Viewer</strong>: A general purpose DICOM Viewer (<a href="http://viewer.ohif.org/">Live Demo</a>)</center>
|
|
||||||
|

|
||||||
|
|
||||||
|
<center><i>The <strong>OHIF Viewer v3</strong>: A general purpose DICOM Viewer (<a href="http://viewer.ohif.org/">Live Demo</a>)</center>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## What's new in `OHIF-v3`
|
||||||
|
`OHIF-v3` is our second try for a React-based viewer, and is the third version of
|
||||||
|
our medical image web viewers from the start. The summary of changes include:
|
||||||
|
|
||||||
|
- Addition of workflow modes
|
||||||
|
|
||||||
|
- Often, medical imaging use cases involves lots of specific workflows that re-use
|
||||||
|
functionalities. We have added the capability of workflow modes, that enable people
|
||||||
|
to customize user interface and configure application for specific workflow.
|
||||||
|
- The idea is to re-use the functionalities that extensions provide and create a workflow.
|
||||||
|
Brain segmentation workflow is different from prostate segmentation in UI for sure; however, they share the segmentation tools that can be re-used.
|
||||||
|
- Our vision is that technical people focus of developing extensions which provides
|
||||||
|
core functionalities, and experts to build modes by picking the appropriate functionalities
|
||||||
|
from each extension.
|
||||||
|
|
||||||
|
|
||||||
|
- Redux store has been removed from the viewer, and a cleaner, more powerful
|
||||||
|
- tailwind CSS
|
||||||
|
- End-to-end test suite
|
||||||
|
|
||||||
|
|
||||||
|
Below, you can find the gap analysis between the `OHIF-v2` and `OHIF-v3`:
|
||||||
|
|
||||||
|
|
||||||
|
{% include "./_gap_analysis.md" %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Where to next?
|
||||||
|
|
||||||
|
|
||||||
The Open Health Imaging Foundation intends to provide a simple general purpose
|
The Open Health Imaging Foundation intends to provide a simple general purpose
|
||||||
DICOM Viewer which can be easily extended for specific uses. If you find
|
DICOM Viewer which can be easily extended for specific uses. If you find
|
||||||
@@ -56,7 +68,6 @@ yourself unable to extend the viewer for your purposes, please reach out via our
|
|||||||
[GitHub issues][gh-issues]. We are actively seeking feedback on ways to improve
|
[GitHub issues][gh-issues]. We are actively seeking feedback on ways to improve
|
||||||
our integration and extension points.
|
our integration and extension points.
|
||||||
|
|
||||||
## Where to next?
|
|
||||||
|
|
||||||
Check out these helpful links:
|
Check out these helpful links:
|
||||||
|
|
||||||
@@ -66,12 +77,19 @@ Check out these helpful links:
|
|||||||
[Learn how you can be more involved.](./development/contributing.md)
|
[Learn how you can be more involved.](./development/contributing.md)
|
||||||
- Feeling lost? Read our [help page](./help.md).
|
- Feeling lost? Read our [help page](./help.md).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Links
|
Links
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<!-- prettier-ignore-start -->
|
<!-- prettier-ignore-start -->
|
||||||
[ohif-org]: http://www.ohif.org
|
[ohif-org]: http://www.ohif.org
|
||||||
|
[ohif-demo]: http://viewer.ohif.org/
|
||||||
[dicom-web]: https://en.wikipedia.org/wiki/DICOMweb
|
[dicom-web]: https://en.wikipedia.org/wiki/DICOMweb
|
||||||
[gh-issues]: https://github.com/OHIF/Viewers/issues
|
[gh-issues]: https://github.com/OHIF/Viewers/issues
|
||||||
<!-- prettier-ignore-end -->
|
<!-- prettier-ignore-end -->
|
||||||