├── .github └── workflows │ └── codacy-analysis.yml ├── .gitignore ├── ACADEMIC.md ├── COMMERCIAL.md ├── LICENSE.md ├── README.md ├── SECURITY.md ├── _config.yml ├── assets ├── banner.png ├── cover.png ├── orthanc_example.png └── orthanc_example.svg ├── build └── main.bundle.js ├── bundle.js ├── gulpfile.js ├── package-lock.json ├── package.json ├── src ├── INSTALL.md ├── app.js ├── architecture.html ├── css │ └── cornerstoneDemo.css ├── favicon.ico ├── index.html ├── js │ ├── about.js │ ├── cornerstoneDemo.js │ ├── disableAllTools.js │ ├── displayThumbnail.js │ ├── forEachViewport.js │ ├── help.js │ ├── imageViewer.js │ ├── loadList.js │ ├── loadStudy.js │ ├── loadTemplate.js │ ├── setupButtons.js │ ├── setupViewport.js │ └── setupViewportOverlays.js ├── lib │ ├── bootstrap.min.css │ ├── bootstrap.min.js │ ├── cornerstone.css │ ├── cornerstone.js │ ├── cornerstone.min.css │ ├── cornerstoneFileImageLoader.js │ ├── cornerstoneMath.js │ ├── cornerstoneTools.js │ ├── cornerstoneWADOImageLoader.js │ ├── cornerstoneWebImageLoader.js │ ├── dicomParser.js │ ├── hammer.min.js │ ├── jpx.js │ ├── jquery-ui.min.css │ ├── jquery-ui.min.js │ ├── jquery.js │ ├── jquery.min.js │ └── jquery.min.map ├── studies │ ├── 0.json │ ├── 10-55-87.json │ ├── 123-45-6789.json │ ├── 3bbf8700-a530-48c7-8ea6-0d782bad0f72.json │ ├── 7.json │ └── Anonymized4.json ├── studyList.json ├── studyViewer.html ├── templates │ ├── about.html │ ├── help.html │ ├── studyViewer.html │ └── viewport.html └── utils │ ├── cors.js │ └── fetchImage.js └── webpack.config.js /.github/workflows/codacy-analysis.yml: -------------------------------------------------------------------------------- 1 | # This workflow checks out code, performs a Codacy security scan 2 | # and integrates the results with the 3 | # GitHub Advanced Security code scanning feature. For more information on 4 | # the Codacy security scan action usage and parameters, see 5 | # https://github.com/codacy/codacy-analysis-cli-action. 6 | # For more information on Codacy Analysis CLI in general, see 7 | # https://github.com/codacy/codacy-analysis-cli. 8 | 9 | name: Codacy Security Scan 10 | 11 | on: 12 | push: 13 | branches: [ master ] 14 | pull_request: 15 | branches: [ master ] 16 | 17 | jobs: 18 | codacy-security-scan: 19 | name: Codacy Security Scan 20 | runs-on: ubuntu-latest 21 | steps: 22 | # Checkout the repository to the GitHub Actions runner 23 | - name: Checkout code 24 | uses: actions/checkout@v2 25 | 26 | # Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis 27 | - name: Run Codacy Analysis CLI 28 | uses: codacy/codacy-analysis-cli-action@1.1.0 29 | with: 30 | # Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository 31 | # You can also omit the token and run the tools that support default configurations 32 | project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} 33 | verbose: true 34 | output: results.sarif 35 | format: sarif 36 | # Adjust severity of non-security issues 37 | gh-code-scanning-compat: true 38 | # Force 0 exit code to allow SARIF file generation 39 | # This will handover control about PR rejection to the GitHub side 40 | max-allowed-issues: 2147483647 41 | 42 | # Upload the SARIF file generated in the previous step 43 | - name: Upload SARIF results file 44 | uses: github/codeql-action/upload-sarif@v1 45 | with: 46 | sarif_file: results.sarif 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /ACADEMIC.md: -------------------------------------------------------------------------------- 1 | # Academic License 2 | 3 | Copyright © 2017 [Instituto Superior Técnico (IST)](https://tecnico.ulisboa.pt/en/) 4 | 5 | Permission for academic and research is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. The content of the present repository has obtained the patent right of [World Intellectual Property Organization (WIPO)](https://www.wipo.int) invention. Moreover, the hereby invention for this repository is under protection of the patent number **[WO2022071818A1](https://patents.google.com/patent/WO2022071818A1)** with the application number **PCT/PT2021/050029**. The title of the invention is "*Computational Method and System for Improved Identification of Breast Lesions*", registered under the WO patent office. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR AN ACADEMIC AND RESEARCH PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /COMMERCIAL.md: -------------------------------------------------------------------------------- 1 | Copyright © 2017 [Instituto Superior Técnico (IST)](https://tecnico.ulisboa.pt/en/) 2 | 3 | # License Summary 4 | 5 | * License does not expire. 6 | * Can not be distributed. 7 | * Non-commercial use only. 8 | * Can not modify source-code for any purpose (can not create derivative works). 9 | 10 | 11 | # MIMBCD-UI Prototype Cornerstone - Terms and conditions 12 | 13 | 1. **Preamble**: This Agreement, signed on Jan 31, 2018 (hereinafter: Effective Date) governs the relationship between Licensee, a Business Entity, (hereinafter: Licensee) and [Instituto Superior Técnico (IST)](https://tecnico.ulisboa.pt/en/) (hereinafter: Licensor). This Agreement sets the terms, rights, restrictions and obligations on using [MIMBCD-UI Prototype Cornerstone](https://github.com/MIMBCD-UI/prototype-cornerstone) (hereinafter: The Software) created and owned by Licensor, as detailed herein. The content of the present repository has obtained the patent right of [World Intellectual Property Organization (WIPO)](https://www.wipo.int) invention. Moreover, the hereby invention for this repository is under protection of the patent number **[WO2022071818A1](https://patents.google.com/patent/WO2022071818A1)** with the application number **PCT/PT2021/050029**. The title of the invention is "*Computational Method and System for Improved Identification of Breast Lesions*", registered under the WO patent office. 14 | 15 | 2. **License Grant**: Licensor hereby grants Licensee a Personal, Non-assignable & non-transferable, Pepetual, Non-commercial, Without the rights to create derivative works, Non-exclusive license, all with accordance with the terms set forth and other legal restrictions set forth in 3rd party software used while running Software. 16 | 17 | 2.1. **Limited**: Licensee may use Software for the purpose of: 18 | 19 | 2.1.1. Running Software on Licensee’s Website[s] and Server[s]; 20 | 21 | 2.1.2. Allowing 3rd Parties to run Software on Licensee’s Website[s] and Server[s]; 22 | 23 | 2.1.3. Publishing Software’s output to Licensee and 3rd Parties; 24 | 25 | 2.1.4. Distribute verbatim copies of Software’s output (including compiled binaries); 26 | 27 | 2.1.5. Modify Software to suit Licensee’s needs and specifications. 28 | 29 | 2.2. This license is granted perpetually, as long as you do not materially breach it. 30 | 31 | 2.3. **Binary Restricted**: Licensee may sublicense Software as a part of a larger work containing more than Software, distributed solely in Object or Binary form under a personal, non-sublicensable, limited license. Such redistribution shall be limited to unlimited codebases. 32 | 33 | 2.4. **Non Assignable & Non-Transferable**: Licensee may not assign or transfer his rights and duties under this license. 34 | 35 | 2.5. **Non-Commercial**: Licensee may not use Software for commercial purposes. for the purpose of this license, commercial purposes means that a 3rd party has to pay in order to access Software or that the Website that runs Software is behind a paywall. 36 | 37 | 3. **Term & Termination**: The Term of this license shall be until terminated. Licensor may terminate this Agreement, including Licensee’s license in the case where Licensee: 38 | 39 | 3.1. became insolvent or otherwise entered into any liquidation process; 40 | 41 | 3.2. exported The Software to any jurisdiction where licensor may not enforce his rights under this agreements in; 42 | 43 | 3.3. Licensee was in breach of any of this license's terms and conditions and such breach was not cured, immediately upon notification; 44 | 45 | 3.4. Licensee in breach of any of the terms of clause 2 to this license; 46 | 47 | 3.5. Licensee otherwise entered into any arrangement which caused Licensor to be unable to enforce his rights under this License. 48 | 49 | 4. **Payment**: In consideration of the License granted under clause 2, Licensee shall pay Licensor a fee, via Credit-Card, PayPal or any other mean which Licensor may deem adequate. Failure to perform payment shall construe as material breach of this Agreement. 50 | 51 | 5. **Upgrades, Updates and Fixes**: Licensor may provide Licensee, from time to time, with Upgrades, Updates or Fixes, as detailed herein and according to his sole discretion. Licensee hereby warrants to keep The Software up-to-date and install all relevant updates and fixes, and may, at his sole discretion, purchase upgrades, according to the rates set by Licensor. Licensor shall provide any update or Fix free of charge; however, nothing in this Agreement shall require Licensor to provide Updates or Fixes. 52 | 53 | 5.1. **Upgrades**: for the purpose of this license, an Upgrade shall be a material amendment in The Software, which contains new features and or major performance improvements and shall be marked as a new version number. For example, should Licensee purchase The Software under version 1.X.X, an upgrade shall commence under number 2.0.0. 54 | 55 | 5.2. **Updates**: for the purpose of this license, an update shall be a minor amendment in The Software, which may contain new features or minor improvements and shall be marked as a new sub-version number. For example, should Licensee purchase The Software under version 1.1.X, an upgrade shall commence under number 1.2.0. 56 | 57 | 5.3. **Fix**: for the purpose of this license, a fix shall be a minor amendment in The Software, intended to remove bugs or alter minor features which impair the The Software's functionality. A fix shall be marked as a new sub-sub-version number. For example, should Licensee purchase Software under version 1.1.1, an upgrade shall commence under number 1.1.2. 58 | 59 | 6. **Support**: Software is provided under an AS-IS basis and without any support, updates or maintenance. Nothing in this Agreement shall require Licensor to provide Licensee with support or fixes to any bug, failure, mis-performance or other defect in The Software. 60 | 61 | 6.1. **Bug Notification**: Licensee may provide Licensor of details regarding any bug, defect or failure in The Software promptly and with no delay from such event; Licensee shall comply with Licensor's request for information regarding bugs, defects or failures and furnish him with information, screenshots and try to reproduce such bugs, defects or failures. 62 | 63 | 6.2. **Feature Request**: Licensee may request additional features in Software, provided, however, that: 64 | 65 | (i) Licensee shall waive any claim or right in such feature should feature be developed by Licensor; 66 | 67 | (ii) Licensee shall be prohibited from developing the feature, or disclose such feature request, or feature, to any 3rd party directly competing with Licensor or any 3rd party which may be, following the development of such feature, in direct competition with Licensor; 68 | 69 | (iii) Licensee warrants that feature does not infringe any 3rd party patent, trademark, trade-secret or any other intellectual property right; 70 | 71 | (iv) Licensee developed, envisioned or created the feature solely by himself. 72 | 73 | 7. **Liability**: To the extent permitted under Law, The Software is provided under an AS-IS basis. Licensor shall never, and without any limit, be liable for any damage, cost, expense or any other payment incurred by Licensee as a result of Software’s actions, failure, bugs and/or any other interaction between The Software and Licensee’s end-equipment, computers, other software or any 3rd party, end-equipment, computer or services. Moreover, Licensor shall never be liable for any defect in source code written by Licensee when relying on The Software or using The Software’s source code. 74 | 75 | 8. **Warranty**: 76 | 77 | 8.1. **Intellectual Property**: Licensor hereby warrants that The Software does not violate or infringe any 3rd party claims in regards to intellectual property, patents and/or trademarks and that to the best of its knowledge no legal action has been taken against it for any infringement or violation of any 3rd party intellectual property rights. 78 | 79 | 8.2. **No-Warranty**: The Software is provided without any warranty; Licensor hereby disclaims any warranty that The Software shall be error free, without defects or code which may cause damage to Licensee’s computers or to Licensee, and that Software shall be functional. Licensee shall be solely liable to any damage, defect or loss incurred as a result of operating software and undertake the risks contained in running The Software on License’s Server[s] and Website[s]. 80 | 81 | 8.3. **Prior Inspection**: Licensee hereby states that he inspected The Software thoroughly and found it satisfactory and adequate to his needs, that it does not interfere with his regular operation and that it does meet the standards and scope of his computer systems and architecture. Licensee found that The Software interacts with his development, website and server environment and that it does not infringe any of End User License Agreement of any software Licensee may use in performing his services. Licensee hereby waives any claims regarding The Software's incompatibility, performance, results and features, and warrants that he inspected the The Software. 82 | 83 | 9. **No Refunds**: Licensee warrants that he inspected The Software according to clause 7(c) and that it is adequate to his needs. Accordingly, as The Software is intangible goods, Licensee shall not be, ever, entitled to any refund, rebate, compensation or restitution for any reason whatsoever, even if The Software contains material flaws. 84 | 85 | 10. **Indemnification**: Licensee hereby warrants to hold Licensor harmless and indemnify Licensor for any lawsuit brought against it in regards to Licensee’s use of The Software in means that violate, breach or otherwise circumvent this license, Licensor's intellectual property rights or Licensor's title in The Software. Licensor shall promptly notify Licensee in case of such legal action and request Licensee’s consent prior to any settlement in relation to such lawsuit or claim. 86 | 87 | 11. **Governing Law, Jurisdiction**: Licensee hereby agrees not to initiate class-action lawsuits against Licensor in relation to this license and to compensate Licensor for any legal fees, cost or attorney fees should any claim brought by Licensee against Licensor be denied, in part or in full. 88 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | Copyright © 2017 [Instituto Superior Técnico (IST)](https://tecnico.ulisboa.pt/en/) 4 | 5 | The [`prototype-cornerstone`](https://github.com/MIMBCD-UI/prototype-cornerstone) repository is distributed under the terms of both [Academic License](https://github.com/MIMBCD-UI/prototype-cornerstone/blob/master/ACADEMIC.md) for academic purpose and [Commercial License](https://github.com/MIMBCD-UI/prototype-cornerstone/blob/master/COMMERCIAL.md) for commercial purpose. The content of the present repository has obtained the patent right of [World Intellectual Property Organization (WIPO)](https://www.wipo.int) invention. Moreover, the hereby invention for this repository is under protection of the patent number **[WO2022071818A1](https://patents.google.com/patent/WO2022071818A1)** with the application number **PCT/PT2021/050029**. The title of the invention is "*Computational Method and System for Improved Identification of Breast Lesions*", registered under the WO patent office. 6 | 7 | See [ACADEMIC](https://github.com/MIMBCD-UI/prototype-cornerstone/blob/master/ACADEMIC.md) and [COMMERCIAL](https://github.com/MIMBCD-UI/prototype-cornerstone/blob/master/COMMERCIAL.md) for details. For more information about the [MIMBCD-UI](https://mimbcd-ui.github.io/) Project just follow the [link](https://github.com/MIMBCD-UI/meta). 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # MIMBCD-UI Cornerstone Prototype 4 | 5 | 6 | 7 | [MIMBCD-UI](https://mimbcd-ui.github.io/) is a Research Project that deals with the use of a recently proposed technique in literature: [Deep Convolutional Neural Networks (CNNs)](https://en.wikipedia.org/wiki/Convolutional_neural_network). These deep networks will incorporate information from several different modes by a User Interface (UI) implemented using the [Cornerstone Library](https://github.com/chafey/cornerstone). The [Cornerstone](https://github.com/chafey/cornerstone) is an Open Source Project with a goal to deliver a complete web based medical imaging platform. A [demo](http://breastscreening.arditi.pt:8982/src/index.html) is also provided to show the source code of this repository in action. 8 | 9 | ## Citing 10 | 11 | We kindly ask **scientific works and studies** that make use of the repository to cite it in their associated publications. Similarly, we ask **open-source** and **closed-source** works that make use of the repository to warn us about this use. 12 | 13 | You can cite our work using the following BibTeX entry: 14 | 15 | ``` 16 | @inproceedings{10.1145/3399715.3399744, 17 | author = {Calisto, Francisco Maria and Nunes, Nuno and Nascimento, Jacinto C.}, 18 | title = {BreastScreening: On the Use of Multi-Modality in Medical Imaging Diagnosis}, 19 | year = {2020}, 20 | isbn = {9781450375351}, 21 | publisher = {Association for Computing Machinery}, 22 | address = {New York, NY, USA}, 23 | url = {https://doi.org/10.1145/3399715.3399744}, 24 | doi = {10.1145/3399715.3399744}, 25 | abstract = {This paper describes the field research, design and comparative deployment of a multimodal medical imaging user interface for breast screening. The main contributions described here are threefold: 1) The design of an advanced visual interface for multimodal diagnosis of breast cancer (BreastScreening); 2) Insights from the field comparison of Single-Modality vs Multi-Modality screening of breast cancer diagnosis with 31 clinicians and 566 images; and 3) The visualization of the two main types of breast lesions in the following image modalities: (i) MammoGraphy (MG) in both Craniocaudal (CC) and Mediolateral oblique (MLO) views; (ii) UltraSound (US); and (iii) Magnetic Resonance Imaging (MRI). We summarize our work with recommendations from the radiologists for guiding the future design of medical imaging interfaces.}, 26 | booktitle = {Proceedings of the International Conference on Advanced Visual Interfaces}, 27 | articleno = {49}, 28 | numpages = {5}, 29 | keywords = {user-centered design, multimodality, medical imaging, human-computer interaction, healthcare systems, breast cancer, annotations}, 30 | location = {Salerno, Italy}, 31 | series = {AVI '20} 32 | } 33 | ``` 34 | 35 | ## Instructions 36 | 37 | First of all, you will need [NodeJS](https://nodejs.org/en/) installed locally on your machine. This project needs both [`npm`](https://www.npmjs.com/) and [`http-server`](https://github.com/indexzero/http-server) dependencies to install and run the core project. If you do not have those installed please follow the [`INSTALL`](src/INSTALL.md) instructions. 38 | 39 | ### Clone 40 | 41 | The following assumes you will be using a [git](https://git-scm.com/) version control for this repository, storing thanks to [GitHub](https://github.com/). First, [Download](https://git-scm.com/downloads) and [Install](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) [git](https://git-scm.com/). 42 | 43 | 1.1. Clone the project repository: 44 | 45 | ``` 46 | git clone git@github.com:MIMBCD-UI/prototype-cornerstone.git 47 | ``` 48 | 49 | 1.2. Go to the project folder: 50 | 51 | ``` 52 | cd prototype-cornerstone/ 53 | ``` 54 | 55 | ### Install 56 | 57 | 2.1. Install the local dependencies: 58 | 59 | ``` 60 | npm install 61 | ``` 62 | 63 | 2.2. You can now **Run** the project, just follow the [next section](https://github.com/MIMBCD-UI/prototype-cornerstone#run). 64 | 65 | ### Run 66 | 67 | 3.1. Inside the project folder: 68 | 69 | `cd prototype-cornerstone/` 70 | 71 | 3.2. Start the DICOM Server (Orthanc) for [MacOS](https://www.orthanc-server.com/static.php?page=download-mac): 72 | 73 | `npm run dicom-server` 74 | 75 | OR 76 | 77 | ``` 78 | cd .. 79 | cd dicom-server/orthancAndPluginsOSX.stable/ 80 | ./startOrthanc.command 81 | ``` 82 | 83 | **NOTE**: If you are not using [MacOS](https://www.orthanc-server.com/static.php?page=download-mac), for instance, if you are using [Windows](https://www.orthanc-server.com/download-windows.php) or [Debian](https://packages.debian.org/search?keywords=orthanc&searchon=names&exact=1&suite=all§ion=all) you have a [documentation](https://www.orthanc-server.com/static.php?page=documentation) for that. Just follow the [Windows](https://www.orthanc-server.com/resources/2015-02-09-emsy-tutorial/index.html) or [Debian](https://packages.debian.org/sid/orthanc) documentations. You also have several [other options](https://www.orthanc-server.com/download.php). 84 | 85 | 86 | 3.3. Open the link: 87 | 88 | ``` 89 | localhost:8042 90 | ``` 91 | 92 | NOTE: If you need some help see the [Demo](https://youtu.be/tkzpT3KpY2A). 93 | 94 | 3.4. Inside the project folder: 95 | 96 | ``` 97 | cd prototype-cornerstone/ 98 | ``` 99 | 100 | 3.5. Run the code: 101 | 102 | ``` 103 | npm start 104 | ``` 105 | 106 | 3.6. Open the link: 107 | 108 | ``` 109 | localhost:8080/src/ 110 | ``` 111 | 112 | #### Allow-Control-Allow-Origin 113 | 114 | Access-Control-Allow-Origin is a [CORS (Cross-Origin Resource Sharing) header](https://www.html5rocks.com/en/tutorials/cors/). If you want to know [How does Access-Control-Allow-Origin header work?](https://stackoverflow.com/questions/10636611/how-does-access-control-allow-origin-header-work) follow the link. 115 | 116 | ##### Google Chrome 117 | 118 | * To deal with the CORS issue it is necessary to open [Google Chrome](https://www.google.com/intl/en/chrome/browser/desktop/) with `--disable-web-security` flag on: 119 | 120 | ``` 121 | open /Applications/Google\ Chrome.app --args --disable-web-security --user-data-dir 122 | ``` 123 | 124 | * Or install the [CORS](https://chrome.google.com/webstore/detail/allow-control-allow-origi/nlfbmbojpeacfghkpbjhddihlkkiljbi?hl=en) plugin for [Google Chrome](https://www.google.com/intl/en/chrome/browser/desktop/). 125 | 126 | 127 | ## About 128 | 129 | For more information about the [MIMBCD-UI](https://mimbcd-ui.github.io/) Project just follow the [link](https://github.com/MIMBCD-UI/meta). Pieces of information about details of this repository are also in a [wiki](https://github.com/MIMBCD-UI/prototype-cornerstone/wiki). This prototype was developed using several libraries and dependencies. Despite that all libraries had their importance and supported the development, one of it was of chief importance. The [CornerstoneJS](https://cornerstonejs.org/) library and [secondary libraries](https://github.com/cornerstonejs), respectively, are supporting this prototype. We [Acknowledge](https://github.com/MIMBCD-UI/prototype-cornerstone/blob/master/README.md#acknowledgments) all people involved in the path. 130 | 131 | ### License 132 | 133 | Copyright © 2017 [Instituto Superior Técnico (IST)](https://tecnico.ulisboa.pt/en/) 134 | 135 | The [`prototype-cornerstone`](https://github.com/MIMBCD-UI/prototype-cornerstone) repository is distributed under the terms of both [Academic License](https://github.com/MIMBCD-UI/prototype-cornerstone/blob/master/ACADEMIC.md) and [Commercial License](https://github.com/MIMBCD-UI/prototype-cornerstone/blob/master/COMMERCIAL.md), for academic and commercial purpose, respectively. For more information regarding the [License](https://github.com/MIMBCD-UI/prototype-cornerstone/blob/master/LICENSE.md) of the hereby repository, just follow both [ACADEMIC](https://github.com/MIMBCD-UI/prototype-cornerstone/blob/master/ACADEMIC.md) and [COMMERCIAL](https://github.com/MIMBCD-UI/prototype-cornerstone/blob/master/COMMERCIAL.md) files. 136 | 137 | #### Copyright 138 | 139 | When source code, documentation and other content is contributed to one of our repositories, the copyrights in those contributions remain owned by the original copyright holders. The copyrights are not assigned to the organization itself. Instead, they are licensed for distribution as part of the project. Whether a project uses new incremental solutions, the original copyright holders retain their copyrights. 140 | 141 | #### Intellectual Property 142 | 143 | The content of the present repository has obtained the patent right of [World Intellectual Property Organization (WIPO)](https://www.wipo.int) invention. Moreover, the hereby invention for this repository is under protection of the patent number **[WO2022071818A1](https://patents.google.com/patent/WO2022071818A1)** with the application number **PCT/PT2021/050029**. The title of the invention is "*Computational Method and System for Improved Identification of Breast Lesions*", registered under the WO patent office. 144 | 145 | ### Acknowledgments 146 | 147 | A special thanks to [Chris Hafey](https://www.linkedin.com/in/chafey/), the propelling person of [CornerstoneJS](https://cornerstonejs.org/), who also developed the [cornerstoneDemo](https://github.com/chafey/cornerstoneDemo). Not forgetting the three supporters of the CornerstoneJS library, [Aloïs Dreyfus](https://www.linkedin.com/in/alois-dreyfus), [Danny Brown](http://dannyrb.com/) and [Erik Ziegler](https://www.npmjs.com/~swederik). We also would like to give a special thanks to [Erik Ziegler](https://www.npmjs.com/~swederik) who support several [issues](https://groups.google.com/forum/#!forum/cornerstone-platform) during this path. 148 | 149 | - [Aloïs Dreyfus](https://www.linkedin.com/in/alois-dreyfus) ([adreyfus](https://github.com/adreyfus)) 150 | 151 | - [Chris Hafey](https://www.linkedin.com/in/chafey/) ([chafey](https://github.com/chafey)) 152 | 153 | - [Danny Brown](http://dannyrb.com/) ([dannyrb](https://github.com/dannyrb)) 154 | 155 | - [Erik Ziegler](https://www.npmjs.com/~swederik) ([swederik](https://github.com/swederik)) 156 | 157 | - [Jason Hostetter](http://www.jasonhostetter.com/) 158 | 159 | - [Marcelo Ribeiro](http://www.sysline.inf.br/) ([sysline](https://github.com/sysline)) 160 | 161 | - [Sébastien Jodogne](https://www.linkedin.com/in/jodogne/) ([jodogne](https://github.com/jodogne)) 162 | 163 | - [Steve Pieper](https://lmi.med.harvard.edu/people/steve-pieper) 164 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MIMBCD-UI/prototype-cornerstone/80de09d7801d693d34936f171d7d0bb965e0f033/assets/banner.png -------------------------------------------------------------------------------- /assets/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MIMBCD-UI/prototype-cornerstone/80de09d7801d693d34936f171d7d0bb965e0f033/assets/cover.png -------------------------------------------------------------------------------- /assets/orthanc_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MIMBCD-UI/prototype-cornerstone/80de09d7801d693d34936f171d7d0bb965e0f033/assets/orthanc_example.png -------------------------------------------------------------------------------- /build/main.bundle.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | /******/ 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | /******/ 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) { 10 | /******/ return installedModules[moduleId].exports; 11 | /******/ } 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ i: moduleId, 15 | /******/ l: false, 16 | /******/ exports: {} 17 | /******/ }; 18 | /******/ 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | /******/ 22 | /******/ // Flag the module as loaded 23 | /******/ module.l = true; 24 | /******/ 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | /******/ 29 | /******/ 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | /******/ 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | /******/ 36 | /******/ // define getter function for harmony exports 37 | /******/ __webpack_require__.d = function(exports, name, getter) { 38 | /******/ if(!__webpack_require__.o(exports, name)) { 39 | /******/ Object.defineProperty(exports, name, { 40 | /******/ configurable: false, 41 | /******/ enumerable: true, 42 | /******/ get: getter 43 | /******/ }); 44 | /******/ } 45 | /******/ }; 46 | /******/ 47 | /******/ // getDefaultExport function for compatibility with non-harmony modules 48 | /******/ __webpack_require__.n = function(module) { 49 | /******/ var getter = module && module.__esModule ? 50 | /******/ function getDefault() { return module['default']; } : 51 | /******/ function getModuleExports() { return module; }; 52 | /******/ __webpack_require__.d(getter, 'a', getter); 53 | /******/ return getter; 54 | /******/ }; 55 | /******/ 56 | /******/ // Object.prototype.hasOwnProperty.call 57 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 58 | /******/ 59 | /******/ // __webpack_public_path__ 60 | /******/ __webpack_require__.p = ""; 61 | /******/ 62 | /******/ // Load entry module and return exports 63 | /******/ return __webpack_require__(__webpack_require__.s = 0); 64 | /******/ }) 65 | /************************************************************************/ 66 | /******/ ([ 67 | /* 0 */ 68 | /***/ (function(module, exports, __webpack_require__) { 69 | 70 | __webpack_require__(1); 71 | module.exports = __webpack_require__(2); 72 | 73 | 74 | /***/ }), 75 | /* 1 */ 76 | /***/ (function(module, exports, __webpack_require__) { 77 | 78 | "use strict"; 79 | 80 | 81 | // TODO 82 | 83 | console.log('cors.js is now loaded...'); 84 | 85 | /***/ }), 86 | /* 2 */ 87 | /***/ (function(module, exports, __webpack_require__) { 88 | 89 | "use strict"; 90 | 91 | 92 | document.write('welcome to my app'); 93 | console.log('app loaded'); 94 | 95 | /***/ }) 96 | /******/ ]); -------------------------------------------------------------------------------- /bundle.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | /******/ 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | /******/ 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) { 10 | /******/ return installedModules[moduleId].exports; 11 | /******/ } 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ i: moduleId, 15 | /******/ l: false, 16 | /******/ exports: {} 17 | /******/ }; 18 | /******/ 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | /******/ 22 | /******/ // Flag the module as loaded 23 | /******/ module.l = true; 24 | /******/ 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | /******/ 29 | /******/ 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | /******/ 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | /******/ 36 | /******/ // define getter function for harmony exports 37 | /******/ __webpack_require__.d = function(exports, name, getter) { 38 | /******/ if(!__webpack_require__.o(exports, name)) { 39 | /******/ Object.defineProperty(exports, name, { 40 | /******/ configurable: false, 41 | /******/ enumerable: true, 42 | /******/ get: getter 43 | /******/ }); 44 | /******/ } 45 | /******/ }; 46 | /******/ 47 | /******/ // getDefaultExport function for compatibility with non-harmony modules 48 | /******/ __webpack_require__.n = function(module) { 49 | /******/ var getter = module && module.__esModule ? 50 | /******/ function getDefault() { return module['default']; } : 51 | /******/ function getModuleExports() { return module; }; 52 | /******/ __webpack_require__.d(getter, 'a', getter); 53 | /******/ return getter; 54 | /******/ }; 55 | /******/ 56 | /******/ // Object.prototype.hasOwnProperty.call 57 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 58 | /******/ 59 | /******/ // __webpack_public_path__ 60 | /******/ __webpack_require__.p = ""; 61 | /******/ 62 | /******/ // Load entry module and return exports 63 | /******/ return __webpack_require__(__webpack_require__.s = 0); 64 | /******/ }) 65 | /************************************************************************/ 66 | /******/ ([ 67 | /* 0 */ 68 | /***/ (function(module, exports, __webpack_require__) { 69 | 70 | document.write('welcome to my app'); 71 | console.log('app loaded'); 72 | 73 | var cors = __webpack_require__(1); 74 | 75 | 76 | var app = express(); 77 | 78 | app.use(cors()); 79 | 80 | app.use(function(req, res, next) { 81 | res.header("Access-Control-Allow-Origin", "*"); 82 | res.header('Access-Control-Allow-Methods', 'DELETE, PUT, GET, POST'); 83 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); 84 | next(); 85 | }); 86 | 87 | 88 | /***/ }), 89 | /* 1 */ 90 | /***/ (function(module, exports, __webpack_require__) { 91 | 92 | (function () { 93 | 94 | 'use strict'; 95 | 96 | var assign = __webpack_require__(2); 97 | var vary = __webpack_require__(3); 98 | 99 | var defaults = { 100 | origin: '*', 101 | methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', 102 | preflightContinue: false, 103 | optionsSuccessStatus: 204 104 | }; 105 | 106 | function isString(s) { 107 | return typeof s === 'string' || s instanceof String; 108 | } 109 | 110 | function isOriginAllowed(origin, allowedOrigin) { 111 | if (Array.isArray(allowedOrigin)) { 112 | for (var i = 0; i < allowedOrigin.length; ++i) { 113 | if (isOriginAllowed(origin, allowedOrigin[i])) { 114 | return true; 115 | } 116 | } 117 | return false; 118 | } else if (isString(allowedOrigin)) { 119 | return origin === allowedOrigin; 120 | } else if (allowedOrigin instanceof RegExp) { 121 | return allowedOrigin.test(origin); 122 | } else { 123 | return !!allowedOrigin; 124 | } 125 | } 126 | 127 | function configureOrigin(options, req) { 128 | var requestOrigin = req.headers.origin, 129 | headers = [], 130 | isAllowed; 131 | 132 | if (!options.origin || options.origin === '*') { 133 | // allow any origin 134 | headers.push([{ 135 | key: 'Access-Control-Allow-Origin', 136 | value: '*' 137 | }]); 138 | } else if (isString(options.origin)) { 139 | // fixed origin 140 | headers.push([{ 141 | key: 'Access-Control-Allow-Origin', 142 | value: options.origin 143 | }]); 144 | headers.push([{ 145 | key: 'Vary', 146 | value: 'Origin' 147 | }]); 148 | } else { 149 | isAllowed = isOriginAllowed(requestOrigin, options.origin); 150 | // reflect origin 151 | headers.push([{ 152 | key: 'Access-Control-Allow-Origin', 153 | value: isAllowed ? requestOrigin : false 154 | }]); 155 | headers.push([{ 156 | key: 'Vary', 157 | value: 'Origin' 158 | }]); 159 | } 160 | 161 | return headers; 162 | } 163 | 164 | function configureMethods(options) { 165 | var methods = options.methods; 166 | if (methods.join) { 167 | methods = options.methods.join(','); // .methods is an array, so turn it into a string 168 | } 169 | return { 170 | key: 'Access-Control-Allow-Methods', 171 | value: methods 172 | }; 173 | } 174 | 175 | function configureCredentials(options) { 176 | if (options.credentials === true) { 177 | return { 178 | key: 'Access-Control-Allow-Credentials', 179 | value: 'true' 180 | }; 181 | } 182 | return null; 183 | } 184 | 185 | function configureAllowedHeaders(options, req) { 186 | var allowedHeaders = options.allowedHeaders || options.headers; 187 | var headers = []; 188 | 189 | if (!allowedHeaders) { 190 | allowedHeaders = req.headers['access-control-request-headers']; // .headers wasn't specified, so reflect the request headers 191 | headers.push([{ 192 | key: 'Vary', 193 | value: 'Access-Control-Request-Headers' 194 | }]); 195 | } else if (allowedHeaders.join) { 196 | allowedHeaders = allowedHeaders.join(','); // .headers is an array, so turn it into a string 197 | } 198 | if (allowedHeaders && allowedHeaders.length) { 199 | headers.push([{ 200 | key: 'Access-Control-Allow-Headers', 201 | value: allowedHeaders 202 | }]); 203 | } 204 | 205 | return headers; 206 | } 207 | 208 | function configureExposedHeaders(options) { 209 | var headers = options.exposedHeaders; 210 | if (!headers) { 211 | return null; 212 | } else if (headers.join) { 213 | headers = headers.join(','); // .headers is an array, so turn it into a string 214 | } 215 | if (headers && headers.length) { 216 | return { 217 | key: 'Access-Control-Expose-Headers', 218 | value: headers 219 | }; 220 | } 221 | return null; 222 | } 223 | 224 | function configureMaxAge(options) { 225 | var maxAge = options.maxAge && options.maxAge.toString(); 226 | if (maxAge && maxAge.length) { 227 | return { 228 | key: 'Access-Control-Max-Age', 229 | value: maxAge 230 | }; 231 | } 232 | return null; 233 | } 234 | 235 | function applyHeaders(headers, res) { 236 | for (var i = 0, n = headers.length; i < n; i++) { 237 | var header = headers[i]; 238 | if (header) { 239 | if (Array.isArray(header)) { 240 | applyHeaders(header, res); 241 | } else if (header.key === 'Vary' && header.value) { 242 | vary(res, header.value); 243 | } else if (header.value) { 244 | res.setHeader(header.key, header.value); 245 | } 246 | } 247 | } 248 | } 249 | 250 | function cors(options, req, res, next) { 251 | var headers = [], 252 | method = req.method && req.method.toUpperCase && req.method.toUpperCase(); 253 | 254 | if (method === 'OPTIONS') { 255 | // preflight 256 | headers.push(configureOrigin(options, req)); 257 | headers.push(configureCredentials(options, req)); 258 | headers.push(configureMethods(options, req)); 259 | headers.push(configureAllowedHeaders(options, req)); 260 | headers.push(configureMaxAge(options, req)); 261 | headers.push(configureExposedHeaders(options, req)); 262 | applyHeaders(headers, res); 263 | 264 | if (options.preflightContinue ) { 265 | next(); 266 | } else { 267 | // Safari (and potentially other browsers) need content-length 0, 268 | // for 204 or they just hang waiting for a body 269 | res.statusCode = options.optionsSuccessStatus || defaults.optionsSuccessStatus; 270 | res.setHeader('Content-Length', '0'); 271 | res.end(); 272 | } 273 | } else { 274 | // actual response 275 | headers.push(configureOrigin(options, req)); 276 | headers.push(configureCredentials(options, req)); 277 | headers.push(configureExposedHeaders(options, req)); 278 | applyHeaders(headers, res); 279 | next(); 280 | } 281 | } 282 | 283 | function middlewareWrapper(o) { 284 | // if options are static (either via defaults or custom options passed in), wrap in a function 285 | var optionsCallback = null; 286 | if (typeof o === 'function') { 287 | optionsCallback = o; 288 | } else { 289 | optionsCallback = function (req, cb) { 290 | cb(null, o); 291 | }; 292 | } 293 | 294 | return function corsMiddleware(req, res, next) { 295 | optionsCallback(req, function (err, options) { 296 | if (err) { 297 | next(err); 298 | } else { 299 | var corsOptions = assign({}, defaults, options); 300 | var originCallback = null; 301 | if (corsOptions.origin && typeof corsOptions.origin === 'function') { 302 | originCallback = corsOptions.origin; 303 | } else if (corsOptions.origin) { 304 | originCallback = function (origin, cb) { 305 | cb(null, corsOptions.origin); 306 | }; 307 | } 308 | 309 | if (originCallback) { 310 | originCallback(req.headers.origin, function (err2, origin) { 311 | if (err2 || !origin) { 312 | next(err2); 313 | } else { 314 | corsOptions.origin = origin; 315 | cors(corsOptions, req, res, next); 316 | } 317 | }); 318 | } else { 319 | next(); 320 | } 321 | } 322 | }); 323 | }; 324 | } 325 | 326 | // can pass either an options hash, an options delegate, or nothing 327 | module.exports = middlewareWrapper; 328 | 329 | }()); 330 | 331 | 332 | /***/ }), 333 | /* 2 */ 334 | /***/ (function(module, exports, __webpack_require__) { 335 | 336 | "use strict"; 337 | /* 338 | object-assign 339 | (c) Sindre Sorhus 340 | @license MIT 341 | */ 342 | 343 | 344 | /* eslint-disable no-unused-vars */ 345 | var getOwnPropertySymbols = Object.getOwnPropertySymbols; 346 | var hasOwnProperty = Object.prototype.hasOwnProperty; 347 | var propIsEnumerable = Object.prototype.propertyIsEnumerable; 348 | 349 | function toObject(val) { 350 | if (val === null || val === undefined) { 351 | throw new TypeError('Object.assign cannot be called with null or undefined'); 352 | } 353 | 354 | return Object(val); 355 | } 356 | 357 | function shouldUseNative() { 358 | try { 359 | if (!Object.assign) { 360 | return false; 361 | } 362 | 363 | // Detect buggy property enumeration order in older V8 versions. 364 | 365 | // https://bugs.chromium.org/p/v8/issues/detail?id=4118 366 | var test1 = new String('abc'); // eslint-disable-line no-new-wrappers 367 | test1[5] = 'de'; 368 | if (Object.getOwnPropertyNames(test1)[0] === '5') { 369 | return false; 370 | } 371 | 372 | // https://bugs.chromium.org/p/v8/issues/detail?id=3056 373 | var test2 = {}; 374 | for (var i = 0; i < 10; i++) { 375 | test2['_' + String.fromCharCode(i)] = i; 376 | } 377 | var order2 = Object.getOwnPropertyNames(test2).map(function (n) { 378 | return test2[n]; 379 | }); 380 | if (order2.join('') !== '0123456789') { 381 | return false; 382 | } 383 | 384 | // https://bugs.chromium.org/p/v8/issues/detail?id=3056 385 | var test3 = {}; 386 | 'abcdefghijklmnopqrst'.split('').forEach(function (letter) { 387 | test3[letter] = letter; 388 | }); 389 | if (Object.keys(Object.assign({}, test3)).join('') !== 390 | 'abcdefghijklmnopqrst') { 391 | return false; 392 | } 393 | 394 | return true; 395 | } catch (err) { 396 | // We don't expect any of the above to throw, but better to be safe. 397 | return false; 398 | } 399 | } 400 | 401 | module.exports = shouldUseNative() ? Object.assign : function (target, source) { 402 | var from; 403 | var to = toObject(target); 404 | var symbols; 405 | 406 | for (var s = 1; s < arguments.length; s++) { 407 | from = Object(arguments[s]); 408 | 409 | for (var key in from) { 410 | if (hasOwnProperty.call(from, key)) { 411 | to[key] = from[key]; 412 | } 413 | } 414 | 415 | if (getOwnPropertySymbols) { 416 | symbols = getOwnPropertySymbols(from); 417 | for (var i = 0; i < symbols.length; i++) { 418 | if (propIsEnumerable.call(from, symbols[i])) { 419 | to[symbols[i]] = from[symbols[i]]; 420 | } 421 | } 422 | } 423 | } 424 | 425 | return to; 426 | }; 427 | 428 | 429 | /***/ }), 430 | /* 3 */ 431 | /***/ (function(module, exports, __webpack_require__) { 432 | 433 | "use strict"; 434 | /*! 435 | * vary 436 | * Copyright(c) 2014-2017 Douglas Christopher Wilson 437 | * MIT Licensed 438 | */ 439 | 440 | 441 | 442 | /** 443 | * Module exports. 444 | */ 445 | 446 | module.exports = vary 447 | module.exports.append = append 448 | 449 | /** 450 | * RegExp to match field-name in RFC 7230 sec 3.2 451 | * 452 | * field-name = token 453 | * token = 1*tchar 454 | * tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" 455 | * / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" 456 | * / DIGIT / ALPHA 457 | * ; any VCHAR, except delimiters 458 | */ 459 | 460 | var FIELD_NAME_REGEXP = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/ 461 | 462 | /** 463 | * Append a field to a vary header. 464 | * 465 | * @param {String} header 466 | * @param {String|Array} field 467 | * @return {String} 468 | * @public 469 | */ 470 | 471 | function append (header, field) { 472 | if (typeof header !== 'string') { 473 | throw new TypeError('header argument is required') 474 | } 475 | 476 | if (!field) { 477 | throw new TypeError('field argument is required') 478 | } 479 | 480 | // get fields array 481 | var fields = !Array.isArray(field) 482 | ? parse(String(field)) 483 | : field 484 | 485 | // assert on invalid field names 486 | for (var j = 0; j < fields.length; j++) { 487 | if (!FIELD_NAME_REGEXP.test(fields[j])) { 488 | throw new TypeError('field argument contains an invalid header name') 489 | } 490 | } 491 | 492 | // existing, unspecified vary 493 | if (header === '*') { 494 | return header 495 | } 496 | 497 | // enumerate current values 498 | var val = header 499 | var vals = parse(header.toLowerCase()) 500 | 501 | // unspecified vary 502 | if (fields.indexOf('*') !== -1 || vals.indexOf('*') !== -1) { 503 | return '*' 504 | } 505 | 506 | for (var i = 0; i < fields.length; i++) { 507 | var fld = fields[i].toLowerCase() 508 | 509 | // append value (case-preserving) 510 | if (vals.indexOf(fld) === -1) { 511 | vals.push(fld) 512 | val = val 513 | ? val + ', ' + fields[i] 514 | : fields[i] 515 | } 516 | } 517 | 518 | return val 519 | } 520 | 521 | /** 522 | * Parse a vary header into an array. 523 | * 524 | * @param {String} header 525 | * @return {Array} 526 | * @private 527 | */ 528 | 529 | function parse (header) { 530 | var end = 0 531 | var list = [] 532 | var start = 0 533 | 534 | // gather tokens 535 | for (var i = 0, len = header.length; i < len; i++) { 536 | switch (header.charCodeAt(i)) { 537 | case 0x20: /* */ 538 | if (start === end) { 539 | start = end = i + 1 540 | } 541 | break 542 | case 0x2c: /* , */ 543 | list.push(header.substring(start, end)) 544 | start = end = i + 1 545 | break 546 | default: 547 | end = i + 1 548 | break 549 | } 550 | } 551 | 552 | // final token 553 | list.push(header.substring(start, end)) 554 | 555 | return list 556 | } 557 | 558 | /** 559 | * Mark that a request is varied on a header field. 560 | * 561 | * @param {Object} res 562 | * @param {String|Array} field 563 | * @public 564 | */ 565 | 566 | function vary (res, field) { 567 | if (!res || !res.getHeader || !res.setHeader) { 568 | // quack quack 569 | throw new TypeError('res argument is required') 570 | } 571 | 572 | // get existing header 573 | var val = res.getHeader('Vary') || '' 574 | var header = Array.isArray(val) 575 | ? val.join(', ') 576 | : String(val) 577 | 578 | // set new header 579 | if ((val = append(header, field))) { 580 | res.setHeader('Vary', val) 581 | } 582 | } 583 | 584 | 585 | /***/ }) 586 | /******/ ]); -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var sync = require('gulp-npm-script-sync'); 3 | var deploy = require('gulp-gh-pages'); 4 | 5 | /** 6 | * Push build to gh-pages 7 | */ 8 | gulp.task('deploy', function () { 9 | return gulp.src("./dist/**/*") 10 | .pipe(deploy()) 11 | }); 12 | 13 | // your gulpfile contents 14 | 15 | sync(gulp); 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prototype-cornerstone", 3 | "version": "0.1.0", 4 | "description": "MIMBCD-UI Prototype Cornerstone", 5 | "keywords": [ 6 | "DICOM", 7 | "WADO", 8 | "cornerstone", 9 | "medical", 10 | "imaging" 11 | ], 12 | "author": "Francisco Maria Calisto", 13 | "homepage": "https://github.com/MIMBCD-UI/prototype-cornerstone", 14 | "license": "MIT", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/MIMBCD-UI/prototype-cornerstone" 18 | }, 19 | "scripts": { 20 | "test": "echo \"Error: no test specified\" && exit 1", 21 | "install": "cd src npm install", 22 | "build": "webpack", 23 | "start": "node node_modules/http-server/bin/http-server -o -p 8982 &", 24 | "dicom-server": "./dicom-server/orthancAndPluginsOSX.stable/startOrthanc.command &", 25 | "webpack": "webpack &", 26 | "webpack-dev-server": "webpack-dev-server &" 27 | }, 28 | "devDependencies": { 29 | "babel-core": "^6.26.3", 30 | "babel-loader": "^8.2.2", 31 | "babel-preset-es2015": "^6.24.1", 32 | "babel-preset-react": "^6.24.1", 33 | "browserify": "^16.5.2", 34 | "grunt-contrib-clean": "^2.0.0", 35 | "grunt-contrib-concat": "^1.0.1", 36 | "grunt-contrib-copy": "^1.0.0", 37 | "grunt-contrib-jshint": "^2.1.0", 38 | "grunt-contrib-qunit": "^3.1.0", 39 | "grunt-contrib-uglify": "^4.0.1", 40 | "grunt-contrib-watch": "^1.1.0", 41 | "gulp-gh-pages": "^0.5.4", 42 | "jshint": "^2.12.0", 43 | "jshint-loader": "^0.8.4", 44 | "load-grunt-tasks": "^5.1.0", 45 | "node-libs-browser": "^2.2.1", 46 | "webpack": "^4.46.0", 47 | "webpack-dev-server": "^4.11.1" 48 | }, 49 | "dependencies": { 50 | "ajv-keywords": "^3.5.2", 51 | "catta": "^2.2.0", 52 | "common-js": "^0.3.8", 53 | "cors": "^2.8.5", 54 | "corsproxy": "^1.5.0", 55 | "ecstatic": "^4.1.4", 56 | "express": "^4.17.1", 57 | "grunt": "^1.5.3", 58 | "gulp-util": "^3.0.8", 59 | "hapi": "^18.1.0", 60 | "hoek": "^6.1.3", 61 | "http-server": "^0.12.3", 62 | "lodash": "^4.17.21", 63 | "semver": "^6.3.0" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/INSTALL.md: -------------------------------------------------------------------------------- 1 | # npm 2 | 3 | A JavaScript package manager. 4 | 5 | ## Fancy Install (Unix) 6 | 7 | There's a pretty robust install script at 8 | . You can download that and run it. 9 | 10 | Here's an example using curl: 11 | 12 | ```sh 13 | curl -L https://www.npmjs.com/install.sh | sh 14 | ``` 15 | 16 | ### Slightly Fancier 17 | 18 | You can set any npm configuration params with that script: 19 | 20 | ```sh 21 | npm_config_prefix=/some/path sh install.sh 22 | ``` 23 | 24 | Or, you can run it in uber-debuggery mode: 25 | 26 | ```sh 27 | npm_debug=1 sh install.sh 28 | ``` 29 | 30 | # HTTP-Server 31 | 32 | The [`http-server`](https://github.com/indexzero/http-server/blob/master/README.md) is a simple, zero-configuration command-line http server. 33 | 34 | ## Installing globally: 35 | 36 | Installation via `npm`: 37 | 38 | npm install http-server -g 39 | 40 | This will install `http-server` globally so that it may be run from the command line. 41 | 42 | ### Usage: 43 | 44 | http-server [path] [options] 45 | 46 | `[path]` defaults to `./public` if the folder exists, and `./` otherwise. 47 | 48 | ## Installing as a node app 49 | 50 | mkdir myapp 51 | cd myapp/ 52 | jitsu install http-server 53 | 54 | *If you do not have `jitsu` installed you can install it via `npm install jitsu -g`* 55 | 56 | ### Usage 57 | 58 | #### Starting http-server locally 59 | 60 | node node_modules/http-server/bin/http-server 61 | 62 | *Now you can visit http://localhost:8081 to view your server* 63 | 64 | ### Available Options: 65 | 66 | `-p` Port to use (defaults to 8080) 67 | 68 | `-a` Address to use (defaults to 0.0.0.0) 69 | 70 | `-d` Show directory listings (defaults to 'True') 71 | 72 | `-i` Display autoIndex (defaults to 'True') 73 | 74 | `-g` or `--gzip` When enabled (defaults to 'False') it will serve `./public/some-file.js.gz` in place of `./public/some-file.js` when a gzipped version of the file exists and the request accepts gzip encoding. 75 | 76 | `-e` or `--ext` Default file extension if none supplied (defaults to 'html') 77 | 78 | `-s` or `--silent` Suppress log messages from output 79 | 80 | `--cors` Enable CORS via the `Access-Control-Allow-Origin` header 81 | 82 | `-o` Open browser window after starting the server 83 | 84 | `-c` Set cache time (in seconds) for cache-control max-age header, e.g. -c10 for 10 seconds (defaults to '3600'). To disable caching, use -c-1. 85 | 86 | `-U` or `--utc` Use UTC time format in log messages. 87 | 88 | `-P` or `--proxy` Proxies all requests which can't be resolved locally to the given url. e.g.: -P http://someurl.com 89 | 90 | `-S` or `--ssl` Enable https. 91 | 92 | `-C` or `--cert` Path to ssl cert file (default: cert.pem). 93 | 94 | `-K` or `--key` Path to ssl key file (default: key.pem). 95 | 96 | `-r` or `--robots` Provide a /robots.txt (whose content defaults to 'User-agent: *\nDisallow: /') 97 | 98 | `-h` or `--help` Print this list and exit. 99 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | document.write('welcome to my app'); 2 | console.log('app loaded'); 3 | -------------------------------------------------------------------------------- /src/architecture.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 |

12 | Architecture 13 |

14 |

15 | This page describes the architecture of the Cornerstone Demo application 16 |

17 | 18 |
19 | 20 |
21 |
Module Dependency Graph
22 |
23 |
zZjdU6MwEMD/mj7a4asfPGq13s3cjc54N949ppCW3AFxQqqtf70LbAIB/ChahT6UbJLdsL/dZGHkLpLdpSB30U8e0njkWOFu5J6PHMf2pz785ZJ9KZlbVinYCBbiIBRsWUgzQyQ5jyW7M4UBT1MaSEO25rGp7I5saEtwE5C4Lb1loYxQaqvl5h3fKNtEaGfuTMuOTO6VjpCuyTaWJ4UI+vLuhChd+Jg7q2ye2LaHRvcocrSt1FjUI+eJIRA0Y4/mwtcMF4ZmVlyEVBiimKX/655zL4CT4Bwm5nfJbkHjnJXCUE5bPtOrHSZoaph+doJbzrgn8RbX3vIgEYI/jNwzUCn2f0Bmq8ZfaFjjSdsoriPjWxGgFgctSSI2FIepcTQ04gCXekl5QsEKDHio0VehGdXAa6GgMZHs3sRA0MEbrVDbuOYM1gwBgPwnShEmwmSKy1Y6ykfCaXXHtjRNTE1eU1PpiJYmuKk9eiUqwD0D0Xs7xLexQqQGK1z+cFj5DQ+72DwYldeArttHQIWePS6qwaWVbdmNvOrLavaJrPAwOS4r1DmgtFJHE3rYnvdlZVuzBiwVBUeANetxkMERbx5k0Nwxqc84uD/oiEOTQ89FtVHqfbPvGec3NDlNTb35/s6ouFr9yytIqJHICurVQuGCi5SKTPKUVvVT2RVJmdegp7kVZ7lhMtquxgEUac4yiMiagk+WQXO6Ch+DCoZWLXLWPJULHvOydnMtuC5g7Fkuv8Fh3kvbwD0VkqpyuzsAlFPnU4w25VTXHecG1YUaX4sXr5Fq9cgwOFROh2bN72/nYP2CN4DsnTxqOjSUIp8GAQXeCxqh/joVNeNLqNyenl99T+D96Acn+TvH++B0atOYiqQfBKbpwZR01fhVuXNOE/5OOpUKjaTYQoaxnc3N7Uz5+7VMaVaIH8sgZODOayKgu4/zW9O144ua8YMc3/JyB4sXc6Er9hWGaWfv52RGq3pzDqjeOuupfp8f6rC+ohSbzfyxX7/MzWviGL3z/p8iJmPfqn7obP0GBb0vmOldw0Gz+pBVDq++ProXTw==
24 |
25 | 26 |
27 | 28 | 29 | 30 |
31 |
Communication Diagram
32 |
33 |
1Vpbb+K6E/80PIKSONweC5TtHrU6lai05/9UGWLA/03iyDGl7KffcTIOuXUXSppSHiAe4/HMb642dMg0eP0mabR9EB7zO47lvXbIrOM4I8uCd004lAgbyb2UZCNhxz0WF0hKCF/xqEhciTBkK1WgrYVfZBbRDasQFivqV6k/uKe2KJwzONLvGN9szTb2YJzOxOpgeHhsTXe+6iYkmNPTATW8UM1XKx3i8gMOzTiiYUGgX0IEBYJkMf9VFHrNUSrcYimkx2SB5PPwZx41cgsmkkLAQv0UvE6Zr81kTJAum78xm4ElWVjY+s0Fw3TFC/V3KHsFvf2WK7aI6EqP9+A7HTLZSOpx2GMqfJEqRGbWzJ33kR2TiqFj1ch01BSckYmAKXnQFkhnXcdFE6RjgiLtj+Z3jW9uc6bv20ikCOgmY31EAB4QhHpACHkPIJcrnQmPSrsWQpnT2jbfyWvtNqE06ti60saMxtLjNpVGcdtW2h2XlB61qLSNafOa4r3k+QSHOTiIoTUf7n+HI97SSD/uAv9mpbT2E60uhxJ1T5fMfxQxV1zo6rAUSumqkH3hxucbPaFEE54zKAI1xMqUB6oGJ7PqEpiMmU+AiQdJQX8fRmSSLL+Jo7RvgBqcUuB5q5TuL2600M585YVOj0OHseYh1NTeCng6c48qCh+aHsNnlDx0WRwDwpz6mtaHijffsyUIxLq2M+pF4aYJJx5jJc2iuhrUDtLy1smIl5jnhKBmHnRVOAxFyNIvaOqfFQYmYieTVHAMF0XlhpkwrIclr3ddsc6IkvlU8ZeiGHVg4B6PgoOAR9xH5WxacvdUfFyV74RKjKDfLTDKxoZRqnSFUWKbTPGTzFXtuaZChkzGKjGMNWOBqMbXngc+hXky+YiqZJt+w6hfk4adOkNmxIvKEvLIQXL39HBfAQH0g20nFFPGCpTT/XQllwTc8/SaSayk+MlyVctKXicBlpjpD11qqV+rxnsdXI0U8WrnMl0svhBYpgNqBaxqb/sPfaHxSvIoDeSvgZndpodhZ3Fd9d7+e72XYg3FHjamfldfSSyp7KY9gNZ9AOfK+Q+2fI6ZBGmeIZMzyLvq+QV2F7KhTmBYPtTUdQJ4wi12AgjyRd6OjM9qBciEvXL1HwxtfP6ftkUPK8IpTUK+I0grx+e1BKRUylwLQTm3Jeib3JExIgaT5psCG90kn9R9feS6PEetRaiMwbWFk3F6V5Xt+rEHuI/L7tVMtUiC+/pRcw1OnwCbuX3MwRarnXfo/T+GpP2pRfHMy5NWUTvhxuijk2tquetJrsTF08y5yXVgYRC9xai51GrMVPb2ex6rL+fxzgBxasXjT7gf+3CPT7a8Ho93Ru9sJ8oeX2HUoMdX28C7p6dHoHy7fbrc2xtw6yxxGzDaPN6ccp/ZzIUZbpT35zdOfa1dmJWOJ7Y96rmk747IaGiNB0Pz8+nZ92dW8QLUtt2ea40GlmP3rT4Z4h1l875uHClnzNn36b8PQPquj5JwMGw2u2OSq/lJZJq82vqNqKHogOHx9+YU/+P/A8jtbw==
34 |
35 | 36 |
37 | 38 |
39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/css/cornerstoneDemo.css: -------------------------------------------------------------------------------- 1 | /* prevent 'bounce' in scrolling */ 2 | html { 3 | height: 100%; 4 | width: 100%; 5 | overflow: hidden; 6 | } 7 | 8 | body { 9 | height: 100%; 10 | width: 100%; 11 | overflow: auto; 12 | color: #888888; 13 | background-color: #060606; 14 | } 15 | 16 | .hidden { 17 | display: none; 18 | } 19 | 20 | .modal-content { 21 | background-color: #202020; 22 | } 23 | 24 | .btn-default { 25 | color: #ffffff; 26 | background-color: #424242; 27 | border-color: #424242; 28 | } 29 | 30 | /* images are displayed in viewports */ 31 | .viewport { 32 | width:100%; 33 | height:100%; 34 | top:0px; 35 | left:0px; 36 | position:absolute 37 | } 38 | 39 | .overlay { 40 | position: absolute; 41 | color: #f1c40f; 42 | } 43 | 44 | .imageViewer { 45 | } 46 | 47 | .myNav { 48 | margin: 0; 49 | border:0; 50 | } 51 | 52 | .viewportWrapper { 53 | box-sizing: border-box; 54 | border: 2px solid #777; 55 | } 56 | 57 | .renderTime {} 58 | 59 | .fps {} 60 | 61 | .csthumbnail { 62 | color: white; 63 | background-color:black; 64 | width:100px; 65 | height:100px; 66 | border: 0px; 67 | padding: 0px; 68 | } 69 | 70 | .viewer { 71 | /*position: absolute;left: 110px;width: 100%;*/ 72 | float: left; 73 | box-sizing: border-box; 74 | } 75 | 76 | .thumbnailSelector { 77 | width:106px; 78 | float:left; 79 | margin-left:0px; 80 | height: 100%; 81 | } 82 | body, html {height:100%} 83 | 84 | #wrap { 85 | height:100% 86 | } 87 | 88 | .studyContainer { 89 | margin-top: 2px; 90 | margin-left:0px; 91 | margin-right:0px; 92 | padding: 0px; 93 | } 94 | 95 | .container { 96 | } 97 | 98 | .navbar-default { 99 | background-color: #060606; 100 | border-color: #000000; 101 | } 102 | 103 | .thumbnails { 104 | margin:0px; 105 | margin-bottom: 0px; 106 | overflow-y:scroll; 107 | overflow-x:hidden; 108 | } 109 | 110 | .table { 111 | border-collapse: collapse; 112 | border-color: gray; 113 | } 114 | 115 | .table>thead>tr>th, 116 | .table>tbody>tr>th, 117 | .table>tfoot>tr>th, 118 | .table>thead>tr>td, 119 | .table>tbody>tr>td, 120 | .table>tfoot>tr>td { 121 | padding: 4px; 122 | border: 1px solid #282828; 123 | } 124 | 125 | .table-striped>tbody>tr:nth-of-type(odd) { 126 | background-color: #202020; 127 | } 128 | 129 | .studyRow { 130 | margin-left: 0px; 131 | margin-rigth: 0px; 132 | height:100%; 133 | } 134 | 135 | .row { 136 | margin:0; 137 | } 138 | 139 | a.list-group-item { 140 | background-color: black; 141 | padding: 2px; 142 | border: 1px solid #424242; 143 | z-index: 5; 144 | } 145 | a.list-group-item.active, 146 | a.list-group-item.active:hover, 147 | a.list-group-item.active:focus { 148 | background-color: #424242; 149 | border-color: #4e4e4e; 150 | } 151 | 152 | .nav-tabs { 153 | border-bottom: 1px solid #424242; 154 | } 155 | 156 | .nav-tabs>li>a { 157 | background-color: #202020; 158 | border-color: #424242; 159 | color: #424242; 160 | padding: 3px 10px; 161 | } 162 | 163 | .nav-tabs>li.active>a, 164 | .nav-tabs>li.active>a:hover, 165 | .nav-tabs>li.active>a:focus { 166 | background-color: #424242; 167 | border-color: #424242; 168 | color: #ffffff; 169 | padding: 3px 10px; 170 | } 171 | 172 | .dropdown-menu { 173 | min-width: 40px; 174 | } 175 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MIMBCD-UI/prototype-cornerstone/80de09d7801d693d34936f171d7d0bb965e0f033/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 | 47 | 48 | 70 | 71 |
72 | 73 | 74 | 77 | 78 | 79 |
80 | 81 |
82 |
83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 |
Patient NamePatient IDStudy DateModalityStudy Description# Images
99 |
100 |
101 |
102 | 103 | 104 |
105 |
106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /src/js/about.js: -------------------------------------------------------------------------------- 1 | loadTemplate("templates/about.html", function(element) { 2 | $('body').append(element); 3 | $("#about").click(function() { 4 | $("#aboutModal").modal(); 5 | }); 6 | }); 7 | -------------------------------------------------------------------------------- /src/js/cornerstoneDemo.js: -------------------------------------------------------------------------------- 1 | // Load in HTML templates 2 | 3 | var viewportTemplate; // the viewport template 4 | loadTemplate("templates/viewport.html", function(element) { 5 | viewportTemplate = element; 6 | }); 7 | 8 | var studyViewerTemplate; // the study viewer template 9 | loadTemplate("templates/studyViewer.html", function(element) { 10 | studyViewerTemplate = element; 11 | }); 12 | 13 | // Get study list from JSON manifest 14 | $.getJSON('studyList.json', function(data) { 15 | data.studyList.forEach(function(study) { 16 | 17 | // Create one table row for each study in the manifest 18 | var studyRow = '' + 19 | study.patientName + '' + 20 | study.patientId + '' + 21 | study.studyDate + '' + 22 | study.modality + '' + 23 | study.studyDescription + '' + 24 | study.numImages + '' + 25 | ''; 26 | 27 | // Append the row to the study list 28 | var studyRowElement = $(studyRow).appendTo('#studyListData'); 29 | 30 | // On study list row click 31 | $(studyRowElement).click(function() { 32 | 33 | // Add new tab for this study and switch to it 34 | var studyTab = '
  • ' + study.patientName + '
  • '; 35 | $('#tabs').append(studyTab); 36 | 37 | // Add tab content by making a copy of the studyViewerTemplate element 38 | var studyViewerCopy = studyViewerTemplate.clone(); 39 | 40 | var viewportCopy = viewportTemplate.clone(); 41 | studyViewerCopy.find('.imageViewer').append(viewportCopy); 42 | 43 | 44 | studyViewerCopy.attr("id", 'x' + study.patientId); 45 | // Make the viewer visible 46 | studyViewerCopy.removeClass('hidden'); 47 | // Add section to the tab content 48 | studyViewerCopy.appendTo('#tabContent'); 49 | 50 | // Show the new tab (which will be the last one since it was just added 51 | $('#tabs a:last').tab('show'); 52 | 53 | // Toggle window resize (?) 54 | $('a[data-toggle="tab"]').on('shown.bs.tab', function(e) { 55 | $(window).trigger('resize'); 56 | }); 57 | 58 | studyViewerCopy.roiData = { 59 | studyId: study.studyId, 60 | modality: study.modality, 61 | stacks: [], 62 | }; 63 | 64 | // Now load the study.json 65 | loadStudy(studyViewerCopy, viewportTemplate, study.studyId + ".json"); 66 | }); 67 | }); 68 | }); 69 | 70 | 71 | // Show tabs on click 72 | $('#tabs a').click (function(e) { 73 | e.preventDefault(); 74 | $(this).tab('show'); 75 | }); 76 | 77 | // Resize main 78 | function resizeMain() { 79 | var height = $(window).height(); 80 | $('#main').height(height - 50); 81 | $('#tabContent').height(height - 50 - 42); 82 | } 83 | 84 | 85 | // Call resize main on window resize 86 | $(window).resize(function() { 87 | resizeMain(); 88 | }); 89 | resizeMain(); 90 | 91 | 92 | // Prevent scrolling on iOS 93 | document.body.addEventListener('touchmove', function(e) { 94 | e.preventDefault(); 95 | }); 96 | -------------------------------------------------------------------------------- /src/js/disableAllTools.js: -------------------------------------------------------------------------------- 1 | // Disable all tools 2 | function disableAllTools() { 3 | forEachViewport(function(element) { 4 | cornerstoneTools.wwwc.disable(element); 5 | cornerstoneTools.pan.activate(element, 2); // 2 is middle mouse button 6 | cornerstoneTools.zoom.activate(element, 4); // 4 is right mouse button 7 | cornerstoneTools.probe.deactivate(element, 1); 8 | cornerstoneTools.length.deactivate(element, 1); 9 | cornerstoneTools.angle.deactivate(element, 1); 10 | cornerstoneTools.ellipticalRoi.deactivate(element, 1); 11 | cornerstoneTools.rectangleRoi.deactivate(element, 1); 12 | cornerstoneTools.freehand.deactivate(element, 1); 13 | cornerstoneTools.stackScroll.deactivate(element, 1); 14 | cornerstoneTools.wwwcTouchDrag.deactivate(element); 15 | cornerstoneTools.zoomTouchDrag.deactivate(element); 16 | cornerstoneTools.panTouchDrag.deactivate(element); 17 | cornerstoneTools.stackScrollTouchDrag.deactivate(element); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /src/js/displayThumbnail.js: -------------------------------------------------------------------------------- 1 | function displayThumbnail(seriesList, seriesElement, element, stack, loaded) { 2 | // Deactivate other thumbnails 3 | $(seriesList).find('a').each(function() { 4 | $(this).removeClass('active'); 5 | }); 6 | 7 | // Make this series visible 8 | 9 | // Make the selected thumbnail active 10 | $(seriesElement).addClass('active'); 11 | 12 | var enabledImage = cornerstone.getEnabledElement(element); 13 | if (enabledImage.image) { 14 | // Stop clip from if playing on element 15 | cornerstoneTools.stopClip(element); 16 | // Disable stack scrolling 17 | cornerstoneTools.stackScroll.disable(element); 18 | // Enable stackScroll on selected series 19 | cornerstoneTools.stackScroll.enable(element); 20 | } 21 | 22 | // Load the first image of the selected series stack 23 | cornerstone.loadAndCacheImage(stack.imageIds[0]).then(function(image) { 24 | if (loaded) { 25 | loaded.call(image, element, stack); 26 | } 27 | 28 | // Get the state of the stack tool 29 | var stackState = cornerstoneTools.getToolState(element, 'stack'); 30 | stackState.data[0] = stack; 31 | stackState.data[0].currentImageIdIndex = 0; 32 | 33 | // Get the default viewport 34 | var defViewport = cornerstone.getDefaultViewport(element, image); 35 | // Get the current series stack index 36 | // Display the image 37 | cornerstone.displayImage(element, image, defViewport); 38 | // Fit the image to the viewport window 39 | cornerstone.fitToWindow(element); 40 | 41 | // Prefetch the remaining images in the stack (?) 42 | cornerstoneTools.stackPrefetch.enable(element); 43 | 44 | // Play clip if stack is a movie (has framerate) 45 | if (stack.frameRate !== undefined) { 46 | cornerstoneTools.playClip(element, stack.frameRate); 47 | } 48 | }); 49 | }; -------------------------------------------------------------------------------- /src/js/forEachViewport.js: -------------------------------------------------------------------------------- 1 | function forEachViewport(callback) { 2 | var elements = $('.viewport'); 3 | $.each(elements, function(index, value) { 4 | var element = value; 5 | try { 6 | callback(element); 7 | } 8 | catch(e) { 9 | 10 | } 11 | }); 12 | } -------------------------------------------------------------------------------- /src/js/help.js: -------------------------------------------------------------------------------- 1 | loadTemplate("templates/help.html", function(element) { 2 | $('body').append(element); 3 | $("#help").click(function() { 4 | $("#helpModal").modal(); 5 | }); 6 | }); 7 | -------------------------------------------------------------------------------- /src/js/imageViewer.js: -------------------------------------------------------------------------------- 1 | ImageViewer = function(root, viewport) { 2 | var self = this; 3 | 4 | self.root = root; 5 | self.stacks = []; 6 | self.viewports = []; 7 | self.layout = '1x1'; 8 | self.viewportModel = viewport; 9 | 10 | self.setLayout = function(layout) { 11 | self.layout = layout; 12 | // TODO: create viewports 13 | var ab = self.getRowsCols(), a = ab[0], b = ab[1], numOfViewports = a * b, 14 | perWidth = 100 / b, perHeight = 100 / a; 15 | self.root.find('.imageViewer').html(''); 16 | var i = 0; 17 | self.viewports = []; 18 | while (i < numOfViewports) { 19 | var elem = self.viewportModel.clone().css({ 20 | width : perWidth + '%', height : perHeight + '%' 21 | }).appendTo(self.root.find('.imageViewer')); 22 | elem.find('.viewport').data('index', i).data('waiting', true); 23 | 24 | self.viewports.push(elem); 25 | i++; 26 | } 27 | } 28 | 29 | self.getRowsCols = function() { 30 | var s = self.layout.split(/x/); 31 | return [parseInt(s[0]), parseInt(s[1])]; 32 | } 33 | 34 | self.isSingle = function() { 35 | return self.layout == '1x1'; 36 | } 37 | 38 | self.getElement = function(item) { 39 | return self.viewports[item].find('.viewport')[0]; 40 | } 41 | 42 | self.forEachElement = function(cb) { 43 | self.viewports.forEach(function(vp, i){ 44 | cb.call(self, vp.find('.viewport')[0], vp, i); 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/js/loadList.js: -------------------------------------------------------------------------------- 1 | function callAPI(url) { 2 | return new Promise((resolve, reject) => { 3 | $.getJSON(url + '&_=' + new Date().getTime(), function(data) { 4 | resolve(data); 5 | }, 6 | function(err) { 7 | reject(err); 8 | }) 9 | }) 10 | } 11 | 12 | async function getStudyList(callback) { 13 | const patients = await callAPI('http://breastscreening.arditi.pt:8550/patients?expand') 14 | const p = patients.reduce((prev, next) => { 15 | return prev.concat(next.Studies); 16 | }, []).map(studyId => { 17 | const url = 'http://breastscreening.arditi.pt:8550/studies/' + studyId + '?'; 18 | return callAPI(url).then(result => result); 19 | }) 20 | 21 | return Promise.all(p).then(result => { 22 | callback(result); 23 | console.log("Promise Result: " + result); 24 | }) 25 | } 26 | 27 | getStudyList((studyList) => { 28 | console.log("Get Study List From: " + studyList); 29 | }) 30 | -------------------------------------------------------------------------------- /src/js/loadStudy.js: -------------------------------------------------------------------------------- 1 | 2 | // Load JSON study information for each study 3 | function loadStudy(studyViewer, viewportModel, studyId) { 4 | 5 | // Get the JSON data for the selected studyId 6 | $.getJSON('studies/' + studyId, function(data) { 7 | 8 | var imageViewer = new ImageViewer(studyViewer, viewportModel); 9 | imageViewer.setLayout('1x1'); // default layout 10 | 11 | function initViewports() { 12 | imageViewer.forEachElement(function(el) { 13 | cornerstone.enable(el); 14 | $(el).droppable({ 15 | drop : function(evt, ui) { 16 | var fromStack = $(ui.draggable.context).data('stack'), toItem = $(this).data('index'); 17 | useItemStack(toItem, fromStack); 18 | } 19 | }); 20 | }); 21 | } 22 | 23 | // setup the tool buttons 24 | setupButtons(studyViewer); 25 | 26 | // layout choose 27 | $(studyViewer).find('.choose-layout a').click(function(){ 28 | var previousUsed = []; 29 | imageViewer.forEachElement(function(el, vp, i){ 30 | if (!isNaN($(el).data('useStack'))) { 31 | previousUsed.push($(el).data('useStack')); 32 | } 33 | }); 34 | 35 | var type = $(this).text(); 36 | imageViewer.setLayout(type); 37 | initViewports(); 38 | resizeStudyViewer(); 39 | if (previousUsed.length > 0) { 40 | previousUsed = previousUsed.slice(0, imageViewer.viewports.length); 41 | var item = 0; 42 | previousUsed.forEach(function(v){ 43 | useItemStack(item++, v); 44 | }); 45 | } 46 | 47 | //return false; 48 | }); 49 | 50 | // Load the first series into the viewport (?) 51 | var stacks = []; 52 | var currentStackIndex = 0; 53 | var seriesIndex = 0; 54 | 55 | // Create a stack object for each series 56 | data.seriesList.forEach(function(series) { 57 | var stack = { 58 | seriesDescription: series.seriesDescription, 59 | stackId: series.seriesNumber, 60 | imageIds: [], 61 | seriesIndex: seriesIndex, 62 | currentImageIdIndex: 0, 63 | frameRate: series.frameRate 64 | }; 65 | 66 | 67 | // Populate imageIds array with the imageIds from each series 68 | // For series with frame information, get the image url's 69 | // by requesting each frame 70 | if (series.numberOfFrames !== undefined) { 71 | var numberOfFrames = series.numberOfFrames; 72 | for (var i = 0; i < numberOfFrames; i++) { 73 | var imageId = series.instanceList[0].imageId + "?frame=" + i; 74 | if (imageId.substr(0, 4) !== 'http') { 75 | //imageId = "dicomweb://cornerstonetech.org/images/ClearCanvas/" + imageId; 76 | //imageId = "dicomweb://breastscreening.arditi.pt:8550/instances/" + imageId; 77 | imageId = "wadouri://breastscreening.arditi.pt:8550/instances/" + imageId; 78 | //imageId = "wadouri://breastscreening.arditi.pt:8550/wado?objectUID=" + imageId + "&requestType=WADO&contentType=application/dicom"; 79 | console.log("DICOM ID: ", imageId); 80 | studyViewer.roiData.dicom_id = imageId; 81 | } 82 | stack.imageIds.push(imageId); 83 | } 84 | // Otherwise, get each instance url 85 | } else { 86 | series.instanceList.forEach(function(image) { 87 | var imageId = image.imageId; 88 | 89 | if (image.imageId.substr(0, 4) !== 'http') { 90 | //imageId = "dicomweb://cornerstonetech.org/images/ClearCanvas/" + image.imageId; 91 | //imageId = "dicomweb://breastscreening.arditi.pt:8550/instances/" + image.imageId; 92 | imageId = "wadouri://breastscreening.arditi.pt:8550/instances/" + image.imageId; 93 | //imageId = "wadouri://breastscreening.arditi.pt:8550/wado?objectUID=" + image.imageId + "&requestType=WADO&contentType=application/dicom"; 94 | console.log("DICOM Image ID: ", image.imageId); 95 | } 96 | stack.imageIds.push(imageId); 97 | }); 98 | } 99 | // Move to next series 100 | seriesIndex++; 101 | 102 | // Add the series stack to the stacks array 103 | imageViewer.stacks.push(stack); 104 | studyViewer.roiData.stacks.push(stack); 105 | }); 106 | 107 | // Resize the parent div of the viewport to fit the screen 108 | var imageViewerElement = $(studyViewer).find('.imageViewer')[0]; 109 | var viewportWrapper = $(imageViewerElement).find('.viewportWrapper')[0]; 110 | var parentDiv = $(studyViewer).find('.viewer')[0]; 111 | 112 | //viewportWrapper.style.width = (parentDiv.style.width - 10) + "px"; 113 | //viewportWrapper.style.height = (window.innerHeight - 150) + "px"; 114 | 115 | var studyRow = $(studyViewer).find('.studyRow')[0]; 116 | var width = $(studyRow).width(); 117 | 118 | //$(parentDiv).width(width - 170); 119 | //viewportWrapper.style.width = (parentDiv.style.width - 10) + "px"; 120 | //viewportWrapper.style.height = (window.innerHeight - 150) + "px"; 121 | 122 | // Get the viewport elements 123 | var element = $(studyViewer).find('.viewport')[0]; 124 | 125 | // Image enable the dicomImage element 126 | initViewports(); 127 | //cornerstone.enable(element); 128 | 129 | // Get series list from the series thumbnails (?) 130 | var seriesList = $(studyViewer).find('.thumbnails')[0]; 131 | imageViewer.stacks.forEach(function(stack, stackIndex) { 132 | 133 | // Create series thumbnail item 134 | var seriesEntry = '' + 139 | '
    ' + 144 | "
    " + stack.seriesDescription + '
    '; 145 | 146 | // Add to series list 147 | var seriesElement = $(seriesEntry).appendTo(seriesList); 148 | 149 | // Find thumbnail 150 | var thumbnail = $(seriesElement).find('div')[0]; 151 | 152 | // Enable cornerstone on the thumbnail 153 | cornerstone.enable(thumbnail); 154 | 155 | // Have cornerstone load the thumbnail image 156 | cornerstone.loadAndCacheImage(imageViewer.stacks[stack.seriesIndex].imageIds[0]).then(function(image) { 157 | // Make the first thumbnail active 158 | if (stack.seriesIndex === 0) { 159 | $(seriesElement).addClass('active'); 160 | } 161 | // Display the image 162 | cornerstone.displayImage(thumbnail, image); 163 | $(seriesElement).draggable({helper: "clone"}); 164 | }); 165 | 166 | // Handle thumbnail click 167 | $(seriesElement).on('click touchstart', function() { 168 | useItemStack(0, stackIndex); 169 | }).data('stack', stackIndex); 170 | }); 171 | 172 | function useItemStack(item, stack) { 173 | studyViewer.roiData.currentStack = stack; 174 | var imageId = imageViewer.stacks[stack].imageIds[0], element = imageViewer.getElement(item); 175 | if ($(element).data('waiting')) { 176 | imageViewer.viewports[item].find('.overlay-text').remove(); 177 | $(element).data('waiting', false); 178 | } 179 | $(element).data('useStack', stack); 180 | 181 | displayThumbnail(seriesList, $(seriesList).find('.list-group-item')[stack], element, imageViewer.stacks[stack], function(el, stack){ 182 | if (!$(el).data('setup')) { 183 | setupViewport(el, stack, this); 184 | setupViewportOverlays(el, data); 185 | $(el).data('setup', true); 186 | } 187 | }); 188 | /*cornerstone.loadAndCacheImage(imageId).then(function(image){ 189 | setupViewport(element, imageViewer.stacks[stack], image); 190 | setupViewportOverlays(element, data); 191 | });*/ 192 | } 193 | // Resize study viewer 194 | function resizeStudyViewer() { 195 | var studyRow = $(studyViewer).find('.studyContainer')[0]; 196 | var height = $(studyRow).height(); 197 | var width = $(studyRow).width();console.log($(studyRow).innerWidth(),$(studyRow).outerWidth(),$(studyRow).width()); 198 | $(seriesList).height("100%"); 199 | $(parentDiv).width(width - $(studyViewer).find('.thumbnailSelector:eq(0)').width()); 200 | $(parentDiv).css({height : '100%'}); 201 | $(imageViewerElement).css({height : $(parentDiv).height() - $(parentDiv).find('.text-center:eq(0)').height()}); 202 | 203 | imageViewer.forEachElement(function(el, vp) { 204 | cornerstone.resize(el, true); 205 | 206 | if ($(el).data('waiting')) { 207 | var ol = vp.find('.overlay-text'); 208 | if (ol.length < 1) { 209 | ol = $('
    Please drag a stack onto here to view images.
    ').appendTo(vp); 210 | } 211 | var ow = vp.width() / 2, oh = vp.height() / 2; 212 | ol.css({top : oh, left : ow - (ol.width() / 2)}); 213 | } 214 | }); 215 | } 216 | // Call resize viewer on window resize 217 | $(window).resize(function() { 218 | resizeStudyViewer(); 219 | }); 220 | resizeStudyViewer(); 221 | if (imageViewer.isSingle()) 222 | useItemStack(0, 0); 223 | 224 | }); 225 | } 226 | -------------------------------------------------------------------------------- /src/js/loadTemplate.js: -------------------------------------------------------------------------------- 1 | function loadTemplate(url, callback) { 2 | $.get(url, function(data) { 3 | var parsed = $.parseHTML(data); 4 | $.each(parsed, function(index, ele) { 5 | if(ele.nodeName === 'DIV') 6 | { 7 | var element = $(ele); 8 | callback(element); 9 | } 10 | }); 11 | }); 12 | 13 | } -------------------------------------------------------------------------------- /src/js/setupButtons.js: -------------------------------------------------------------------------------- 1 | 2 | function setupButtons(studyViewer) { 3 | // Get the button elements 4 | var buttons = $(studyViewer).find('button'); 5 | 6 | // Tool button event handlers that set the new active tool 7 | 8 | // WW/WL 9 | $(buttons[0]).on('click touchstart', function() { 10 | disableAllTools(); 11 | forEachViewport(function(element) { 12 | cornerstoneTools.wwwc.activate(element, 1); 13 | cornerstoneTools.wwwcTouchDrag.activate(element); 14 | }); 15 | }); 16 | 17 | // Invert 18 | $(buttons[1]).on('click touchstart', function() { 19 | disableAllTools(); 20 | forEachViewport(function(element) { 21 | var viewport = cornerstone.getViewport(element); 22 | // Toggle invert 23 | if (viewport.invert === true) { 24 | viewport.invert = false; 25 | } else { 26 | viewport.invert = true; 27 | } 28 | cornerstone.setViewport(element, viewport); 29 | }); 30 | }); 31 | 32 | // Zoom 33 | $(buttons[2]).on('click touchstart', function() { 34 | disableAllTools(); 35 | forEachViewport(function(element) { 36 | // 5 is right mouse button and left mouse button 37 | cornerstoneTools.zoom.activate(element, 5); 38 | cornerstoneTools.zoomTouchDrag.activate(element); 39 | }); 40 | }); 41 | 42 | // Pan 43 | $(buttons[3]).on('click touchstart', function() { 44 | disableAllTools(); 45 | forEachViewport(function(element) { 46 | // 3 is middle mouse button and left mouse button 47 | cornerstoneTools.pan.activate(element, 3); 48 | cornerstoneTools.panTouchDrag.activate(element); 49 | }); 50 | }); 51 | 52 | // Stack scroll 53 | $(buttons[4]).on('click touchstart', function() { 54 | disableAllTools(); 55 | forEachViewport(function(element) { 56 | cornerstoneTools.stackScroll.activate(element, 1); 57 | cornerstoneTools.stackScrollTouchDrag.activate(element); 58 | }); 59 | }); 60 | 61 | // Length measurement 62 | $(buttons[5]).on('click touchstart', function() { 63 | disableAllTools(); 64 | forEachViewport(function(element) { 65 | cornerstoneTools.length.activate(element, 1); 66 | }); 67 | }); 68 | 69 | // Freehand ROI draw 70 | $(buttons[6]).on('click touchstart', function() { 71 | forEachViewport(function(element) { 72 | cornerstoneTools.freehand.activate(element, 1); 73 | }); 74 | }); 75 | 76 | // Tooltips 77 | $(buttons[0]).tooltip(); 78 | $(buttons[1]).tooltip(); 79 | $(buttons[2]).tooltip(); 80 | $(buttons[3]).tooltip(); 81 | $(buttons[4]).tooltip(); 82 | $(buttons[5]).tooltip(); 83 | $(buttons[8]).tooltip(); 84 | 85 | }; 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /src/js/setupViewport.js: -------------------------------------------------------------------------------- 1 | function setupViewport(element, stack, image) { 2 | // Display the image on the viewer element 3 | cornerstone.displayImage(element, image); 4 | 5 | // If it's a movie (has frames), then play the clip 6 | if (stack.frameRate !== undefined) { 7 | cornerstone.playClip(element, stack.frameRate); 8 | } 9 | 10 | // Activate mouse clicks, mouse wheel and touch 11 | cornerstoneTools.mouseInput.enable(element); 12 | cornerstoneTools.mouseWheelInput.enable(element); 13 | cornerstoneTools.touchInput.enable(element); 14 | 15 | // Enable all tools we want to use with this element 16 | cornerstoneTools.wwwc.activate(element, 1); // ww/wc is the default tool for left mouse button 17 | cornerstoneTools.pan.activate(element, 2); // pan is the default tool for middle mouse button 18 | cornerstoneTools.zoom.activate(element, 4); // zoom is the default tool for right mouse button 19 | cornerstoneTools.probe.enable(element); 20 | cornerstoneTools.length.enable(element); 21 | cornerstoneTools.ellipticalRoi.enable(element); 22 | cornerstoneTools.rectangleRoi.enable(element); 23 | cornerstoneTools.wwwcTouchDrag.activate(element); 24 | cornerstoneTools.zoomTouchPinch.activate(element); 25 | cornerstoneTools.freehand.activate(element); 26 | 27 | // Stack tools 28 | cornerstoneTools.addStackStateManager(element, ['playClip']); 29 | cornerstoneTools.addToolState(element, 'stack', stack); 30 | cornerstoneTools.stackScrollWheel.activate(element); 31 | cornerstoneTools.stackPrefetch.enable(element); 32 | 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/js/setupViewportOverlays.js: -------------------------------------------------------------------------------- 1 | function setupViewportOverlays(element, data) { 2 | var parent = $(element).parent(); 3 | 4 | // Get the overlays 5 | var childDivs = $(parent).find('.overlay'); 6 | var topLeft = $(childDivs[0]).find('div'); 7 | var topRight = $(childDivs[1]).find('div'); 8 | var bottomLeft = $(childDivs[2]).find('div'); 9 | var bottomRight = $(childDivs[3]).find('div'); 10 | 11 | // Set the overlay text 12 | $(topLeft[0]).text(data.patientName); 13 | $(topLeft[1]).text(data.patientId); 14 | $(topRight[0]).text(data.studyDescription); 15 | $(topRight[1]).text(data.studyDate); 16 | 17 | 18 | // On new image (displayed?) 19 | function onNewImage(e, eventData) { 20 | // If we are currently playing a clip then update the FPS 21 | // Get the state of the 'playClip tool' 22 | var playClipToolData = cornerstoneTools.getToolState(element, 'playClip'); 23 | 24 | // If playing a clip ... 25 | if (playClipToolData !== undefined && playClipToolData.data.length > 0 && playClipToolData.data[0].intervalId !== undefined && eventData.frameRate !== undefined) { 26 | 27 | // Update FPS 28 | $(bottomLeft[0]).text("FPS: " + Math.round(eventData.frameRate)); 29 | console.log('frameRate: ' + e.frameRate); 30 | 31 | } else { 32 | // Set FPS empty if not playing a clip 33 | if ($(bottomLeft[0]).text().length > 0) { 34 | $(bottomLeft[0]).text(""); 35 | } 36 | } 37 | 38 | var toolData = cornerstoneTools.getToolState(element, 'stack'); 39 | if(toolData === undefined || toolData.data === undefined || toolData.data.length === 0) { 40 | return; 41 | } 42 | var stack = toolData.data[0]; 43 | 44 | // Update Image number overlay 45 | $(bottomLeft[2]).text("Image # " + (stack.currentImageIdIndex + 1) + "/" + stack.imageIds.length); 46 | } 47 | // Add a CornerstoneNewImage event listener on the 'element' (viewer) (?) 48 | $(element).on("CornerstoneNewImage", onNewImage); 49 | 50 | 51 | // On image rendered 52 | function onImageRendered(e, eventData) { 53 | // Set zoom overlay text 54 | $(bottomRight[0]).text("Zoom:" + eventData.viewport.scale.toFixed(2)); 55 | // Set WW/WL overlay text 56 | $(bottomRight[1]).text("WW/WL:" + Math.round(eventData.viewport.voi.windowWidth) + "/" + Math.round(eventData.viewport.voi.windowCenter)); 57 | // Set render time overlay text 58 | $(bottomLeft[1]).text("Render Time:" + eventData.renderTimeInMs + " ms"); 59 | } 60 | // Add a CornerstoneImageRendered event listener on the 'element' (viewer) (?) 61 | $(element).on("CornerstoneImageRendered", onImageRendered); 62 | 63 | 64 | } -------------------------------------------------------------------------------- /src/lib/cornerstone.css: -------------------------------------------------------------------------------- 1 | /*! cornerstone - v0.7.3 - 2015-04-04 | (c) 2014 Chris Hafey | https://github.com/chafey/cornerstone */ 2 | .cornerstone-enabled-image { 3 | 4 | /* prevent text selection from occurring when dragging the mouse on the div */ 5 | /* http://stackoverflow.com/questions/826782/css-rule-to-disable-text-selection-highlighting */ 6 | -webkit-touch-callout: none; 7 | -webkit-user-select: none; 8 | -khtml-user-select: none; 9 | -moz-user-select: none; 10 | -ms-user-select: none; 11 | user-select: none; 12 | 13 | /* force the cursor to always be the default arrow. This prevents it from changing to an ibar when it is 14 | over HTML based text overlays (four corners */ 15 | cursor:default; 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/lib/cornerstone.min.css: -------------------------------------------------------------------------------- 1 | /*! cornerstone - v0.8.1 - 2015-06-16 | (c) 2014 Chris Hafey | https://github.com/chafey/cornerstone */.cornerstone-enabled-image{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;cursor:default} -------------------------------------------------------------------------------- /src/lib/cornerstoneFileImageLoader.js: -------------------------------------------------------------------------------- 1 | /*! cornerstone-file-image-loader - v0.5.1 - 2015-04-04 | (c) 2014 Chris Hafey | https://github.com/chafey/cornerstoneFileImageLoader */ 2 | // 3 | // This is a cornerstone image loader for DICOM P10 files. It currently does not support compressed 4 | // transfer syntaxes or big endian transfer syntaxes. It will support implicit little endian transfer 5 | // syntaxes but explicit little endian is strongly preferred to avoid any parsing issues related 6 | // to SQ elements. 7 | // 8 | 9 | var cornerstoneFileImageLoader = (function ($, cornerstone, cornerstoneFileImageLoader) { 10 | 11 | "use strict"; 12 | 13 | if(cornerstoneFileImageLoader === undefined) { 14 | cornerstoneFileImageLoader = {}; 15 | } 16 | 17 | 18 | 19 | function isColorImage(photoMetricInterpretation) 20 | { 21 | if(photoMetricInterpretation === "RGB" || 22 | photoMetricInterpretation === "PALETTE COLOR" || 23 | photoMetricInterpretation === "YBR_FULL" || 24 | photoMetricInterpretation === "YBR_FULL_422" || 25 | photoMetricInterpretation === "YBR_PARTIAL_422" || 26 | photoMetricInterpretation === "YBR_PARTIAL_420" || 27 | photoMetricInterpretation === "YBR_RCT") 28 | { 29 | return true; 30 | } 31 | else 32 | { 33 | return false; 34 | } 35 | } 36 | 37 | function createImageObject(dataSet, imageId, frame) 38 | { 39 | if(frame === undefined) { 40 | frame = 0; 41 | } 42 | 43 | // make the image based on whether it is color or not 44 | var photometricInterpretation = dataSet.string('x00280004'); 45 | var isColor = isColorImage(photometricInterpretation); 46 | if(isColor === false) { 47 | return cornerstoneWADOImageLoader.makeGrayscaleImage(imageId, dataSet, dataSet.byteArray, photometricInterpretation, frame); 48 | } else { 49 | return cornerstoneWADOImageLoader.makeColorImage(imageId, dataSet, dataSet.byteArray, photometricInterpretation, frame); 50 | } 51 | } 52 | 53 | var multiFrameCacheHack = {}; 54 | 55 | // Loads an image given an imageId 56 | // wado url example: 57 | // http://localhost:3333/wado?requestType=WADO&studyUID=1.3.6.1.4.1.25403.166563008443.5076.20120418075541.1&seriesUID=1.3.6.1.4.1.25403.166563008443.5076.20120418075541.2&objectUID=1.3.6.1.4.1.25403.166563008443.5076.20120418075557.1&contentType=application%2Fdicom&transferSyntax=1.2.840.10008.1.2.1 58 | // NOTE: supposedly the instance will be returned in Explicit Little Endian transfer syntax if you don't 59 | // specify a transferSyntax but Osirix doesn't do this and seems to return it with the transfer syntax it is 60 | // stored as. 61 | function loadImage(imageId) { 62 | // create a deferred object 63 | // TODO: Consider not using jquery for deferred - maybe cujo's when library 64 | var deferred = $.Deferred(); 65 | 66 | // build a url by parsing out the url scheme and frame index from the imageId 67 | var url = imageId; 68 | url = url.substring(12); 69 | var frameIndex = url.indexOf('frame='); 70 | var frame; 71 | if(frameIndex !== -1) { 72 | var frameStr = url.substr(frameIndex + 6); 73 | frame = parseInt(frameStr); 74 | url = url.substr(0, frameIndex-1); 75 | } 76 | 77 | // if multiframe and cached, use the cached data set to extract the frame 78 | if(frame !== undefined && 79 | multiFrameCacheHack.hasOwnProperty(url)) 80 | { 81 | var dataSet = multiFrameCacheHack[url]; 82 | var imagePromise = createImageObject(dataSet, imageId, frame); 83 | imagePromise.then(function(image) { 84 | deferred.resolve(image); 85 | }, function() { 86 | deferred.reject(); 87 | }); 88 | return deferred; 89 | } 90 | 91 | var fileIndex = parseInt(url); 92 | var file = cornerstoneFileImageLoader.getFile(fileIndex); 93 | if(file === undefined) { 94 | deferred.reject('unknown file index ' + url); 95 | return deferred; 96 | } 97 | 98 | // Read the DICOM Data 99 | var fileReader = new FileReader(); 100 | fileReader.onload = function(e) { 101 | // Parse the DICOM File 102 | var dicomPart10AsArrayBuffer = e.target.result; 103 | var byteArray = new Uint8Array(dicomPart10AsArrayBuffer); 104 | var dataSet = dicomParser.parseDicom(byteArray); 105 | 106 | // if multiframe, cache the parsed data set to speed up subsequent 107 | // requests for the other frames 108 | if(frame !== undefined) { 109 | multiFrameCacheHack[url] = dataSet; 110 | } 111 | 112 | var imagePromise = createImageObject(dataSet, imageId, frame); 113 | imagePromise.then(function(image) { 114 | deferred.resolve(image); 115 | }, function() { 116 | deferred.reject(); 117 | }); 118 | }; 119 | fileReader.readAsArrayBuffer(file); 120 | 121 | return deferred; 122 | } 123 | 124 | // steam the http and https prefixes so we can use wado URL's directly 125 | cornerstone.registerImageLoader('dicomfile', loadImage); 126 | 127 | return cornerstoneFileImageLoader; 128 | }($, cornerstone, cornerstoneFileImageLoader)); 129 | /** 130 | */ 131 | var cornerstoneFileImageLoader = (function (cornerstoneFileImageLoader) { 132 | 133 | "use strict"; 134 | 135 | if(cornerstoneFileImageLoader === undefined) { 136 | cornerstoneFileImageLoader = {}; 137 | } 138 | 139 | var files = []; 140 | 141 | function addFile(file) { 142 | var fileIndex = files.push(file); 143 | return fileIndex - 1; 144 | } 145 | 146 | function getFile(index) { 147 | return files[index]; 148 | } 149 | 150 | function purge() { 151 | files = []; 152 | } 153 | 154 | // module exports 155 | cornerstoneFileImageLoader.addFile = addFile; 156 | cornerstoneFileImageLoader.getFile = getFile; 157 | cornerstoneFileImageLoader.purge = purge; 158 | 159 | return cornerstoneFileImageLoader; 160 | }(cornerstoneFileImageLoader)); -------------------------------------------------------------------------------- /src/lib/cornerstoneWebImageLoader.js: -------------------------------------------------------------------------------- 1 | /*! cornerstoneWebImageLoader - v0.5.2 - 2015-09-06 | (c) 2015 Chris Hafey | https://github.com/chafey/cornerstoneWebImageLoader */ 2 | cornerstoneWebImageLoader = {}; 3 | // 4 | // This is a cornerstone image loader for web images such as PNG and JPEG 5 | // 6 | 7 | (function ($, cornerstone, cornerstoneWebImageLoader) { 8 | 9 | "use strict"; 10 | 11 | var canvas = document.createElement('canvas'); 12 | var lastImageIdDrawn = ""; 13 | 14 | 15 | var options = { 16 | // callback allowing customization of the xhr (e.g. adding custom auth headers, cors, etc) 17 | beforeSend : function(xhr) {} 18 | }; 19 | 20 | function createImageObject(image, imageId) 21 | { 22 | // extract the attributes we need 23 | var rows = image.naturalHeight; 24 | var columns = image.naturalWidth; 25 | 26 | function getPixelData() 27 | { 28 | var imageData = getImageData(); 29 | var imageDataData = imageData.data; 30 | var numPixels = image.naturalHeight * image.naturalWidth; 31 | var storedPixelData = new Uint8Array(numPixels * 4); 32 | var imageDataIndex = 0; 33 | var storedPixelDataIndex = 0; 34 | for(var i=0; i < numPixels; i++) { 35 | storedPixelData[storedPixelDataIndex++] = imageDataData[imageDataIndex++]; 36 | storedPixelData[storedPixelDataIndex++] = imageDataData[imageDataIndex++]; 37 | storedPixelData[storedPixelDataIndex++] = imageDataData[imageDataIndex++]; 38 | storedPixelData[storedPixelDataIndex++] = 255; // alpha 39 | imageDataIndex++; 40 | } 41 | return storedPixelData; 42 | } 43 | 44 | function getImageData() 45 | { 46 | var context; 47 | if(lastImageIdDrawn !== imageId) { 48 | canvas.height = image.naturalHeight; 49 | canvas.width = image.naturalWidth; 50 | context = canvas.getContext('2d'); 51 | context.drawImage(image, 0, 0); 52 | lastImageIdDrawn = imageId; 53 | } 54 | else { 55 | context = canvas.getContext('2d'); 56 | } 57 | var imageData = context.getImageData(0, 0, image.naturalWidth, image.naturalHeight); 58 | return imageData; 59 | } 60 | 61 | function getCanvas() 62 | { 63 | if(lastImageIdDrawn === imageId) { 64 | return canvas; 65 | } 66 | 67 | canvas.height = image.naturalHeight; 68 | canvas.width = image.naturalWidth; 69 | var context = canvas.getContext('2d'); 70 | context.drawImage(image, 0, 0); 71 | lastImageIdDrawn = imageId; 72 | return canvas; 73 | } 74 | 75 | function getImage() 76 | { 77 | return image; 78 | } 79 | 80 | // Extract the various attributes we need 81 | var imageObject = { 82 | imageId : imageId, 83 | minPixelValue : 0, // calculated below 84 | maxPixelValue : 255, // calculated below 85 | slope: 1.0, 86 | intercept: 0, 87 | windowCenter : 127, 88 | windowWidth : 256, 89 | render: cornerstone.renderColorImage, 90 | getPixelData: getPixelData, 91 | getImageData: getImageData, 92 | getCanvas: getCanvas, 93 | getImage: getImage, 94 | //storedPixelData: extractStoredPixels(image), 95 | rows: rows, 96 | columns: columns, 97 | height: rows, 98 | width: columns, 99 | color: true, 100 | columnPixelSpacing: 1.0, 101 | rowPixelSpacing: 1.0, 102 | invert: false, 103 | sizeInBytes : rows * columns * 4 // we don't know for sure so we over estimate to be safe 104 | }; 105 | 106 | return imageObject; 107 | } 108 | 109 | // Loads an image given a url to an image 110 | function loadImage(imageId) { 111 | 112 | // create a deferred object 113 | var deferred = $.Deferred(); 114 | 115 | var image = new Image(); 116 | 117 | var xhr = new XMLHttpRequest(); 118 | xhr.responseType = "arraybuffer"; 119 | xhr.open("GET", imageId, true); 120 | options.beforeSend(xhr); 121 | xhr.onload = function(e) { 122 | var arrayBufferView = new Uint8Array(this.response); 123 | var blob = new Blob([arrayBufferView], {type: "image/jpeg"}); 124 | var urlCreator = window.URL || window.webkitURL; 125 | var imageUrl = urlCreator.createObjectURL(blob); 126 | image.src = imageUrl; 127 | image.onload = function() { 128 | var imageObject = createImageObject(image, imageId); 129 | deferred.resolve(imageObject); 130 | urlCreator.revokeObjectURL(imageUrl); 131 | }; 132 | image.onerror = function() { 133 | urlCreator.revokeObjectURL(imageUrl); 134 | deferred.reject(); 135 | }; 136 | } 137 | xhr.onprogress = function(oProgress) { 138 | 139 | if (oProgress.lengthComputable) { //evt.loaded the bytes browser receive 140 | //evt.total the total bytes seted by the header 141 | // 142 | var loaded = oProgress.loaded; 143 | var total = oProgress.total; 144 | var percentComplete = Math.round((loaded / total)*100); 145 | 146 | $(cornerstone).trigger('CornerstoneImageLoadProgress', { 147 | imageId: imageId, 148 | loaded: loaded, 149 | total: total, 150 | percentComplete: percentComplete 151 | }); 152 | } 153 | }; 154 | xhr.send(); 155 | return deferred; 156 | } 157 | 158 | function configure(opts) { 159 | options = opts; 160 | } 161 | 162 | // steam the http and https prefixes so we can use standard web urls directly 163 | cornerstone.registerImageLoader('http', loadImage); 164 | cornerstone.registerImageLoader('https', loadImage); 165 | cornerstoneWebImageLoader.configure = configure; 166 | return cornerstone; 167 | }($, cornerstone, cornerstoneWebImageLoader)); 168 | -------------------------------------------------------------------------------- /src/lib/hammer.min.js: -------------------------------------------------------------------------------- 1 | /*! Hammer.JS - v2.0.4 - 2014-09-28 2 | * http://hammerjs.github.io/ 3 | * 4 | * Copyright (c) 2014 Jorik Tangelder; 5 | * Licensed under the MIT license */ 6 | !function(a,b,c,d){"use strict";function e(a,b,c){return setTimeout(k(a,c),b)}function f(a,b,c){return Array.isArray(a)?(g(a,c[b],c),!0):!1}function g(a,b,c){var e;if(a)if(a.forEach)a.forEach(b,c);else if(a.length!==d)for(e=0;e-1}function r(a){return a.trim().split(/\s+/g)}function s(a,b,c){if(a.indexOf&&!c)return a.indexOf(b);for(var d=0;dc[b]}):d.sort()),d}function v(a,b){for(var c,e,f=b[0].toUpperCase()+b.slice(1),g=0;g1&&!c.firstMultiple?c.firstMultiple=E(b):1===e&&(c.firstMultiple=!1);var f=c.firstInput,g=c.firstMultiple,h=g?g.center:f.center,i=b.center=F(d);b.timeStamp=nb(),b.deltaTime=b.timeStamp-f.timeStamp,b.angle=J(h,i),b.distance=I(h,i),C(c,b),b.offsetDirection=H(b.deltaX,b.deltaY),b.scale=g?L(g.pointers,d):1,b.rotation=g?K(g.pointers,d):0,D(c,b);var j=a.element;p(b.srcEvent.target,j)&&(j=b.srcEvent.target),b.target=j}function C(a,b){var c=b.center,d=a.offsetDelta||{},e=a.prevDelta||{},f=a.prevInput||{};(b.eventType===yb||f.eventType===Ab)&&(e=a.prevDelta={x:f.deltaX||0,y:f.deltaY||0},d=a.offsetDelta={x:c.x,y:c.y}),b.deltaX=e.x+(c.x-d.x),b.deltaY=e.y+(c.y-d.y)}function D(a,b){var c,e,f,g,h=a.lastInterval||b,i=b.timeStamp-h.timeStamp;if(b.eventType!=Bb&&(i>xb||h.velocity===d)){var j=h.deltaX-b.deltaX,k=h.deltaY-b.deltaY,l=G(i,j,k);e=l.x,f=l.y,c=mb(l.x)>mb(l.y)?l.x:l.y,g=H(j,k),a.lastInterval=b}else c=h.velocity,e=h.velocityX,f=h.velocityY,g=h.direction;b.velocity=c,b.velocityX=e,b.velocityY=f,b.direction=g}function E(a){for(var b=[],c=0;ce;)c+=a[e].clientX,d+=a[e].clientY,e++;return{x:lb(c/b),y:lb(d/b)}}function G(a,b,c){return{x:b/a||0,y:c/a||0}}function H(a,b){return a===b?Cb:mb(a)>=mb(b)?a>0?Db:Eb:b>0?Fb:Gb}function I(a,b,c){c||(c=Kb);var d=b[c[0]]-a[c[0]],e=b[c[1]]-a[c[1]];return Math.sqrt(d*d+e*e)}function J(a,b,c){c||(c=Kb);var d=b[c[0]]-a[c[0]],e=b[c[1]]-a[c[1]];return 180*Math.atan2(e,d)/Math.PI}function K(a,b){return J(b[1],b[0],Lb)-J(a[1],a[0],Lb)}function L(a,b){return I(b[0],b[1],Lb)/I(a[0],a[1],Lb)}function M(){this.evEl=Nb,this.evWin=Ob,this.allow=!0,this.pressed=!1,y.apply(this,arguments)}function N(){this.evEl=Rb,this.evWin=Sb,y.apply(this,arguments),this.store=this.manager.session.pointerEvents=[]}function O(){this.evTarget=Ub,this.evWin=Vb,this.started=!1,y.apply(this,arguments)}function P(a,b){var c=t(a.touches),d=t(a.changedTouches);return b&(Ab|Bb)&&(c=u(c.concat(d),"identifier",!0)),[c,d]}function Q(){this.evTarget=Xb,this.targetIds={},y.apply(this,arguments)}function R(a,b){var c=t(a.touches),d=this.targetIds;if(b&(yb|zb)&&1===c.length)return d[c[0].identifier]=!0,[c,c];var e,f,g=t(a.changedTouches),h=[],i=this.target;if(f=c.filter(function(a){return p(a.target,i)}),b===yb)for(e=0;eh&&(b.push(a),h=b.length-1):e&(Ab|Bb)&&(c=!0),0>h||(b[h]=a,this.callback(this.manager,e,{pointers:b,changedPointers:[a],pointerType:f,srcEvent:a}),c&&b.splice(h,1))}});var Tb={touchstart:yb,touchmove:zb,touchend:Ab,touchcancel:Bb},Ub="touchstart",Vb="touchstart touchmove touchend touchcancel";j(O,y,{handler:function(a){var b=Tb[a.type];if(b===yb&&(this.started=!0),this.started){var c=P.call(this,a,b);b&(Ab|Bb)&&c[0].length-c[1].length===0&&(this.started=!1),this.callback(this.manager,b,{pointers:c[0],changedPointers:c[1],pointerType:tb,srcEvent:a})}}});var Wb={touchstart:yb,touchmove:zb,touchend:Ab,touchcancel:Bb},Xb="touchstart touchmove touchend touchcancel";j(Q,y,{handler:function(a){var b=Wb[a.type],c=R.call(this,a,b);c&&this.callback(this.manager,b,{pointers:c[0],changedPointers:c[1],pointerType:tb,srcEvent:a})}}),j(S,y,{handler:function(a,b,c){var d=c.pointerType==tb,e=c.pointerType==vb;if(d)this.mouse.allow=!1;else if(e&&!this.mouse.allow)return;b&(Ab|Bb)&&(this.mouse.allow=!0),this.callback(a,b,c)},destroy:function(){this.touch.destroy(),this.mouse.destroy()}});var Yb=v(jb.style,"touchAction"),Zb=Yb!==d,$b="compute",_b="auto",ac="manipulation",bc="none",cc="pan-x",dc="pan-y";T.prototype={set:function(a){a==$b&&(a=this.compute()),Zb&&(this.manager.element.style[Yb]=a),this.actions=a.toLowerCase().trim()},update:function(){this.set(this.manager.options.touchAction)},compute:function(){var a=[];return g(this.manager.recognizers,function(b){l(b.options.enable,[b])&&(a=a.concat(b.getTouchAction()))}),U(a.join(" "))},preventDefaults:function(a){if(!Zb){var b=a.srcEvent,c=a.offsetDirection;if(this.manager.session.prevented)return void b.preventDefault();var d=this.actions,e=q(d,bc),f=q(d,dc),g=q(d,cc);return e||f&&c&Hb||g&&c&Ib?this.preventSrc(b):void 0}},preventSrc:function(a){this.manager.session.prevented=!0,a.preventDefault()}};var ec=1,fc=2,gc=4,hc=8,ic=hc,jc=16,kc=32;V.prototype={defaults:{},set:function(a){return h(this.options,a),this.manager&&this.manager.touchAction.update(),this},recognizeWith:function(a){if(f(a,"recognizeWith",this))return this;var b=this.simultaneous;return a=Y(a,this),b[a.id]||(b[a.id]=a,a.recognizeWith(this)),this},dropRecognizeWith:function(a){return f(a,"dropRecognizeWith",this)?this:(a=Y(a,this),delete this.simultaneous[a.id],this)},requireFailure:function(a){if(f(a,"requireFailure",this))return this;var b=this.requireFail;return a=Y(a,this),-1===s(b,a)&&(b.push(a),a.requireFailure(this)),this},dropRequireFailure:function(a){if(f(a,"dropRequireFailure",this))return this;a=Y(a,this);var b=s(this.requireFail,a);return b>-1&&this.requireFail.splice(b,1),this},hasRequireFailures:function(){return this.requireFail.length>0},canRecognizeWith:function(a){return!!this.simultaneous[a.id]},emit:function(a){function b(b){c.manager.emit(c.options.event+(b?W(d):""),a)}var c=this,d=this.state;hc>d&&b(!0),b(),d>=hc&&b(!0)},tryEmit:function(a){return this.canEmit()?this.emit(a):void(this.state=kc)},canEmit:function(){for(var a=0;af?Db:Eb,c=f!=this.pX,d=Math.abs(a.deltaX)):(e=0===g?Cb:0>g?Fb:Gb,c=g!=this.pY,d=Math.abs(a.deltaY))),a.direction=e,c&&d>b.threshold&&e&b.direction},attrTest:function(a){return Z.prototype.attrTest.call(this,a)&&(this.state&fc||!(this.state&fc)&&this.directionTest(a))},emit:function(a){this.pX=a.deltaX,this.pY=a.deltaY;var b=X(a.direction);b&&this.manager.emit(this.options.event+b,a),this._super.emit.call(this,a)}}),j(_,Z,{defaults:{event:"pinch",threshold:0,pointers:2},getTouchAction:function(){return[bc]},attrTest:function(a){return this._super.attrTest.call(this,a)&&(Math.abs(a.scale-1)>this.options.threshold||this.state&fc)},emit:function(a){if(this._super.emit.call(this,a),1!==a.scale){var b=a.scale<1?"in":"out";this.manager.emit(this.options.event+b,a)}}}),j(ab,V,{defaults:{event:"press",pointers:1,time:500,threshold:5},getTouchAction:function(){return[_b]},process:function(a){var b=this.options,c=a.pointers.length===b.pointers,d=a.distanceb.time;if(this._input=a,!d||!c||a.eventType&(Ab|Bb)&&!f)this.reset();else if(a.eventType&yb)this.reset(),this._timer=e(function(){this.state=ic,this.tryEmit()},b.time,this);else if(a.eventType&Ab)return ic;return kc},reset:function(){clearTimeout(this._timer)},emit:function(a){this.state===ic&&(a&&a.eventType&Ab?this.manager.emit(this.options.event+"up",a):(this._input.timeStamp=nb(),this.manager.emit(this.options.event,this._input)))}}),j(bb,Z,{defaults:{event:"rotate",threshold:0,pointers:2},getTouchAction:function(){return[bc]},attrTest:function(a){return this._super.attrTest.call(this,a)&&(Math.abs(a.rotation)>this.options.threshold||this.state&fc)}}),j(cb,Z,{defaults:{event:"swipe",threshold:10,velocity:.65,direction:Hb|Ib,pointers:1},getTouchAction:function(){return $.prototype.getTouchAction.call(this)},attrTest:function(a){var b,c=this.options.direction;return c&(Hb|Ib)?b=a.velocity:c&Hb?b=a.velocityX:c&Ib&&(b=a.velocityY),this._super.attrTest.call(this,a)&&c&a.direction&&a.distance>this.options.threshold&&mb(b)>this.options.velocity&&a.eventType&Ab},emit:function(a){var b=X(a.direction);b&&this.manager.emit(this.options.event+b,a),this.manager.emit(this.options.event,a)}}),j(db,V,{defaults:{event:"tap",pointers:1,taps:1,interval:300,time:250,threshold:2,posThreshold:10},getTouchAction:function(){return[ac]},process:function(a){var b=this.options,c=a.pointers.length===b.pointers,d=a.distance