Skip to content

Commit 9cbda68

Browse files
committed
Ensure save works for both folders and manifests
1 parent dc1dcce commit 9cbda68

File tree

1 file changed

+238
-50
lines changed

1 file changed

+238
-50
lines changed

main.js

Lines changed: 238 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,11 @@ async function loadDicomsUsingDcm2niixFromManifest(manifestUrl) {
7373
* @param manifestURL - URL to a text manifest with relative DICOM file paths
7474
*/
7575
async 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
10199
const nv = new Niivue({ dragAndDropEnabled: false })
100+
101+
// reference to the results list from dcm2niix for use later
102102
let resultFileList = []
103+
let conversionTime = 0
103104
let 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

131171
const 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

160328
async 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

Comments
 (0)