@@ -73,12 +73,11 @@ async function loadDicomsUsingDcm2niixFromManifest(manifestUrl) {
7373 * @param manifestURL - URL to a text manifest with relative DICOM file paths
7474 */
7575async function loadDicomsWithNiivueLoader ( manifestURL ) {
76- console . log ( 'loading manifest' )
76+ console . log ( 'Loading DICOM manifest via dicom-loader...' )
77+ showLoadingCircle ( )
7778 const startTime = performance . now ( )
7879
79- nv . useDicomLoader ( {
80- loader : dicomLoader
81- } )
80+ nv . useDicomLoader ( { loader : dicomLoader } )
8281
8382 await nv . loadDicoms ( [
8483 {
@@ -87,92 +86,281 @@ async function loadDicomsWithNiivueLoader(manifestURL) {
8786 }
8887 ] )
8988
90- const vol = nv . volumes [ nv . volumes . length - 1 ]
89+ const vol = nv . volumes [ nv . volumes . length - 1 ]
9190 const name = vol ?. name || 'Unnamed volume'
9291 const endTime = performance . now ( )
9392 const elapsed = ( ( endTime - startTime ) / 1000 ) . toFixed ( 2 )
93+ hideLoadingCircle ( )
9494 showText ( `Loaded ${ name } in ${ elapsed } seconds` )
9595 showSaveButton ( )
9696}
9797
98-
99-
100- // Page-wide variables
98+ // NiiVue instance
10199const nv = new Niivue ( { dragAndDropEnabled : false } )
100+
101+ // reference to the results list from dcm2niix for use later
102102let resultFileList = [ ]
103+ let conversionTime = 0
103104let downloadFile = null
104105
105- const showText = ( text ) => {
106- document . getElementById ( 'intensity' ) . innerHTML = text
107- }
108- const showSaveButton = ( ) => document . getElementById ( 'saveButton' ) . classList . remove ( 'hidden' )
109- const hideSaveButton = ( ) => document . getElementById ( 'saveButton' ) . classList . add ( 'hidden' )
110- const showLoadingCircle = ( ) => loadingCircle . classList . remove ( 'hidden' )
111- const hideLoadingCircle = ( ) => loadingCircle . classList . add ( 'hidden' )
112- const showFileSelect = ( ) => fileSelect . classList . remove ( 'hidden' )
113- const hideFileSelect = ( ) => fileSelect . classList . add ( 'hidden' )
114- const removeAllVolumes = ( ) => nv . volumes . forEach ( v => nv . removeVolume ( v ) )
115- const removeSelectItems = ( ) => { while ( fileSelect . firstChild ) fileSelect . removeChild ( fileSelect . firstChild ) }
116- const updateSelectItems = ( files ) => {
117- removeSelectItems ( )
118- files . forEach ( ( file , i ) => {
119- const option = document . createElement ( 'option' )
120- option . value = i
121- option . text = file . name
122- fileSelect . appendChild ( option )
123- } )
124- const option = document . createElement ( 'option' )
125- option . value = - 1
126- option . text = 'Select a file'
127- option . selected = true
128- fileSelect . appendChild ( option )
106+
107+ const handleSaveButtonClick = async ( ) => {
108+
109+ if ( nv . volumes . length === 0 ) {
110+ if ( downloadFile ) {
111+ let url = URL . createObjectURL ( downloadFile ) ;
112+ const downloadLink = document . createElement ( 'a' ) ;
113+ downloadLink . href = url ;
114+ downloadLink . download = downloadFile . name ;
115+ downloadLink . click ( )
116+
117+ }
118+ else {
119+ console . log ( 'no volumes found' )
120+ }
121+ return
122+ }
123+ const vol = nv . volumes [ 0 ]
124+ const name = vol . name || 'volume'
125+ const ext = vol . niiFile ?. name ?. endsWith ( '.nii.gz' ) ? '.nii.gz' : '.nii'
126+ console . log ( 'saving ' , name )
127+ await nv . saveImage ( { filename : `${ name } ${ ext } ` } )
128+ }
129+
130+ const showSaveButton = ( ) => {
131+ const saveButton = document . getElementById ( 'saveButton' )
132+ saveButton . classList . remove ( 'hidden' )
133+ }
134+
135+ const hideSaveButton = ( ) => {
136+ const saveButton = document . getElementById ( 'saveButton' )
137+ saveButton . classList . add ( 'hidden' )
138+ }
139+
140+ const showLoadingCircle = ( ) => {
141+ loadingCircle . classList . remove ( 'hidden' )
142+ }
143+
144+ const hideLoadingCircle = ( ) => {
145+ loadingCircle . classList . add ( 'hidden' )
146+ }
147+
148+ const hideFileSelect = ( ) => {
149+ fileSelect . classList . add ( 'hidden' )
150+ }
151+
152+ const showFileSelect = ( ) => {
153+ fileSelect . classList . remove ( 'hidden' )
154+ }
155+
156+ const handleLocationChange = ( data ) => {
157+ document . getElementById ( "intensity" ) . innerHTML = data . string
158+ }
159+
160+ const showText = ( time ) => {
161+ document . getElementById ( "intensity" ) . innerHTML = time
162+ }
163+
164+ const removeAllVolumes = ( ) => {
165+ const vols = nv . volumes
166+ for ( let i = 0 ; i < vols . length ; i ++ ) {
167+ nv . removeVolume ( vols [ i ] )
168+ }
129169}
130170
131171const handleFileSelectChange = async ( event ) => {
132- if ( resultFileList . length === 0 ) return
172+ if ( resultFileList . length === 0 ) {
173+ console . log ( 'No files to select from' ) ;
174+ return
175+ }
133176 const selectedIndex = parseInt ( event . target . value )
134- if ( selectedIndex === - 1 ) return
177+ if ( selectedIndex === - 1 ) {
178+ return
179+ }
135180 const selectedFile = resultFileList [ selectedIndex ]
136- downloadFile = selectedFile
181+ downloadFile = selectedFile
182+ // only load the file in niivue if it is nifti
137183 if ( selectedFile . name . endsWith ( '.nii' ) ) {
138184 removeAllVolumes ( )
185+ console . log ( selectedFile ) ;
139186 const image = await NVImage . loadFromFile ( {
140187 file : selectedFile ,
141188 name : selectedFile . name
142189 } )
143- nv . addVolume ( image )
190+ await nv . addVolume ( image )
144191 }
145192 showSaveButton ( )
146193}
147194
148- const handleSaveButtonClick = async ( ) => {
149- if ( nv . volumes . length === 0 ) {
150- console . log ( 'no volumes found' )
151- return
195+ const removeSelectItems = ( ) => {
196+ const select = document . getElementById ( 'fileSelect' )
197+ // remove all options elements
198+ while ( select . firstChild ) {
199+ select . removeChild ( select . firstChild )
200+ }
201+ }
202+
203+ const updateSelectItems = ( files ) => {
204+ removeSelectItems ( )
205+ const select = document . getElementById ( 'fileSelect' )
206+ select . innerHTML = ''
207+ for ( let i = 0 ; i < files . length ; i ++ ) {
208+ const option = document . createElement ( 'option' )
209+ option . value = i
210+ option . text = files [ i ] . name
211+ select . appendChild ( option )
212+ }
213+ // make first option say 'Select a file'
214+ const option = document . createElement ( 'option' )
215+ option . value = - 1
216+ option . text = 'Select a file'
217+ option . selected = true
218+ select . appendChild ( option )
219+ }
220+
221+ const runDcm2niix = async ( files ) => {
222+ try {
223+ hideSaveButton ( )
224+ showLoadingCircle ( )
225+ const dcm2niix = new Dcm2niix ( ) ;
226+ await dcm2niix . init ( ) ;
227+ const t0 = Date . now ( )
228+ resultFileList = await dcm2niix . input ( files ) . run ( )
229+ const t1 = Date . now ( )
230+ conversionTime = ( t1 - t0 ) / 1000
231+ showText ( `Conversion time: ${ conversionTime } seconds` )
232+ // filter out files that are not nifti (.nii) so we don't show them
233+ // in the select dropdown
234+ // resultFileList = resultFileList.filter(file => file.name.endsWith('.nii'))
235+ updateSelectItems ( resultFileList )
236+ console . log ( resultFileList ) ;
237+ hideLoadingCircle ( )
238+ showFileSelect ( )
239+ // set the first file as the selected file
240+ fileSelect . value = 0
241+ // trigger the change event
242+ const event = new Event ( 'change' )
243+ fileSelect . dispatchEvent ( event )
244+ } catch ( error ) {
245+ console . error ( error ) ;
246+ resultFileList = [ ]
247+ hideLoadingCircle ( )
248+ hideFileSelect ( )
249+ showText ( 'Error converting files. Check the console for more information.' )
152250 }
153- const vol = nv . volumes [ 0 ]
154- const name = vol . name || 'volume'
155- const ext = vol . niiFile ?. name ?. endsWith ( '.nii.gz' ) ? '.nii.gz' : '.nii'
156- console . log ( 'saving ' , name )
157- await nv . saveImage ( { filename : `${ name } ${ ext } ` } )
251+ }
252+
253+ const ensureObjectOfObjects = ( obj ) => {
254+ // check for the "length" property
255+ if ( obj . length ) {
256+ return obj
257+ } else {
258+ return { 0 : obj }
259+ }
260+ }
261+
262+ async function handleDrop ( e ) {
263+ e . preventDefault ( ) ; // prevent navigation to open file
264+ const items = e . dataTransfer . items ;
265+ try {
266+ showLoadingCircle ( )
267+ const files = [ ] ;
268+ for ( let i = 0 ; i < items . length ; i ++ ) {
269+ const item = items [ i ] . webkitGetAsEntry ( ) ;
270+ if ( item ) {
271+ await traverseFileTree ( item , '' , files ) ;
272+ }
273+ }
274+ const dcm2niix = new Dcm2niix ( ) ;
275+ await dcm2niix . init ( )
276+ resultFileList = await dcm2niix . inputFromDropItems ( files ) . run ( )
277+ resultFileList = resultFileList . filter ( file => file . name . endsWith ( '.nii' ) )
278+ updateSelectItems ( resultFileList )
279+ console . log ( resultFileList ) ;
280+ hideLoadingCircle ( )
281+ showFileSelect ( )
282+ // set the first file as the selected file
283+ fileSelect . value = 0
284+ // trigger the change event
285+ const event = new Event ( 'change' )
286+ fileSelect . dispatchEvent ( event )
287+ showText ( '' )
288+ } catch ( error ) {
289+ console . error ( error ) ;
290+ hideLoadingCircle ( )
291+ hideFileSelect ( )
292+ showText ( 'Error converting files. Check the console for more information.' )
293+ }
294+ }
295+
296+ async function traverseFileTree ( item , path = '' , fileArray ) {
297+ return new Promise ( ( resolve ) => {
298+ if ( item . isFile ) {
299+ item . file ( file => {
300+ file . fullPath = path + file . name ;
301+ // IMPORTANT: _webkitRelativePath is required for dcm2niix to work.
302+ // We need to add this property so we can parse multiple directories correctly.
303+ // the "webkitRelativePath" property on File objects is read-only, so we can't set it directly, hence the underscore.
304+ file . _webkitRelativePath = path + file . name ;
305+ fileArray . push ( file ) ;
306+ resolve ( ) ;
307+ } ) ;
308+ } else if ( item . isDirectory ) {
309+ const dirReader = item . createReader ( ) ;
310+ const readAllEntries = ( ) => {
311+ dirReader . readEntries ( entries => {
312+ if ( entries . length > 0 ) {
313+ const promises = [ ] ;
314+ for ( const entry of entries ) {
315+ promises . push ( traverseFileTree ( entry , path + item . name + '/' , fileArray ) ) ;
316+ }
317+ Promise . all ( promises ) . then ( readAllEntries ) ;
318+ } else {
319+ resolve ( ) ;
320+ }
321+ } ) ;
322+ } ;
323+ readAllEntries ( ) ;
324+ }
325+ } ) ;
158326}
159327
160328async function main ( ) {
329+ fileInput . addEventListener ( 'change' , async ( event ) => {
330+ if ( event . target . files . length === 0 ) {
331+ console . log ( 'No files selected' ) ;
332+ return ;
333+ }
334+ console . log ( 'Selected files:' , event . target . files ) ;
335+ const selectedFiles = event . target . files ;
336+ const files = ensureObjectOfObjects ( selectedFiles ) // probably not needed anymore with new dcm2niix version
337+ await runDcm2niix ( files )
338+ } ) ;
339+
340+ // when user changes the file to view
161341 fileSelect . onchange = handleFileSelectChange
342+
343+ // handle drag and drop
344+ dropTarget . ondrop = handleDrop ;
345+ dropTarget . ondragover = ( e ) => { e . preventDefault ( ) ; }
346+
347+ // when user clicks save
162348 saveButton . onclick = handleSaveButtonClick
349+
350+ // when a user clicks load manifest
163351 document . getElementById ( 'loadManifestBtn' ) . onclick = ( ) => {
164352 loadDicomsWithNiivueLoader ( 'https://niivue.github.io/niivue-demo-images/dicom/niivue-manifest.txt' )
165353 }
166354
167- nv . onLocationChange = ( data ) => showText ( data . string )
168- nv . onVolumeAdded = ( vol ) => showText ( `Loaded: ${ vol . name } ` )
169-
355+ // crosshair location change event
356+ nv . onLocationChange = handleLocationChange
357+ // get canvas element
170358 const canvas = document . getElementById ( 'gl' )
171359 nv . attachToCanvas ( canvas )
172-
360+ // set some options
173361 nv . opts . yoke3Dto2DZoom = true
174362 nv . opts . crosshairGap = 5
175- nv . setInterpolation ( true )
363+ nv . setInterpolation ( true ) // linear
176364 nv . setMultiplanarLayout ( MULTIPLANAR_TYPE . GRID )
177365 nv . setSliceType ( SLICE_TYPE . MULTIPLANAR )
178366 nv . opts . multiplanarShowRender = SHOW_RENDER . ALWAYS
0 commit comments