diff --git a/index.html b/index.html index eaa9bef..7b4445d 100644 --- a/index.html +++ b/index.html @@ -1,15 +1,6 @@ - - - @@ -64,7 +55,7 @@ - + SimTIVA | Free TIVA Simulation App to Simulate Target-Controlled Infusion | Simple TCI Calculator @@ -82,7 +73,7 @@

SimTIVA is a computer simulation program to simulate delivery of total intra
Written by Terence Luk, 2023. This work is licensed under GNU General Public License v3.0. Read more about the project here, or contact me on Twitter/X for ideas, suggestions or comments. Your advice is greatly appreciated!

This is an open source project and the source code is published on GitHub. -
Last updated 1/11/2023 (V4.1) Build 90. +
Last updated 1/12/2023 (V4.2) Build 91.

The purposes are: (1) To simulate TCI/TIVA for educational purposes, and (2) Potentially, to help deliver TCI/TIVA in a low resource setting with no TCI pumps available.
Coding is done in Javascript. The code to the mathematical calculations are based on "STANPUMP", which is freely available from the link below. The pharmacokinetic models available in this program are Marsh, Schnider, Paedfusor and Eleveld for propofol, and Minto and Eleveld for remifentanil. For instructions on using this app, visit the 'Help' page. For documentation of the pharmacological details, visit the 'Documentation' page. @@ -450,16 +441,35 @@

SimTIVA is a computer simulation program to simulate delivery of total intra @@ -1144,7 +1154,7 @@

SimTIVA is a computer simulation program to simulate delivery of total intra
REMINDER - next change in
10s
-
_
+
_
@@ -1152,7 +1162,7 @@

SimTIVA is a computer simulation program to simulate delivery of total intra
PREVIEW: To achieve target
-
+

@@ -1161,7 +1171,7 @@

SimTIVA is a computer simulation program to simulate delivery of total intra
Current rate
-
Waiting to start
+
Waiting to start

diff --git a/main.js b/main.js index b5ef68f..2b179c8 100644 --- a/main.js +++ b/main.js @@ -220,7 +220,12 @@ screen.orientation.addEventListener("change", (event) => { //for tooltip destruction on touchend for mobile devices document.addEventListener('touchend', function(event) { if (event.target && event.target.tagName.toLowerCase() !== "canvas") { - myChart.canvas.dispatchEvent(new Event("mouseout")); + if (popupon) { + popupchart.canvas.dispatchEvent(new Event("mouseout")); + } else { + myChart.canvas.dispatchEvent(new Event("mouseout")); + } + } }) @@ -1723,7 +1728,7 @@ function displaypreview2(x,ind) { preview_cpt(x,ind); document.getElementById("preview_cpt").innerHTML = "CP " + x + drug_sets[ind].conc_units + "/ml"; if (drug_sets[ind].preview_bolus>0) { - document.getElementById("preview_msg").innerHTML = "Bolus " + drug_sets[ind].preview_bolus + drug_sets[ind].infused_units + " (" + Math.round(drug_sets[ind].preview_bolus / drug_sets[ind].infusate_concentration * 10)/10 + "ml) then infuse at " + drug_sets[ind].preview_rate + "ml/h"; + document.getElementById("preview_msg").innerHTML = "Bolus " + drug_sets[ind].preview_bolus + drug_sets[ind].infused_units + " (" + Math.round(drug_sets[ind].preview_bolus / drug_sets[ind].infusate_concentration * 10)/10 + "ml) " + "" + " Infuse at " + drug_sets[ind].preview_rate + "ml/h"; document.getElementById("previewicon").className = "fas fa-arrow-circle-up"; } else if (drug_sets[ind].preview_rate>0) { document.getElementById("preview_msg").innerHTML = "Change infusion rate to " + drug_sets[ind].preview_rate + "ml/h"; @@ -3392,7 +3397,7 @@ function apply_fentanyl_correction(ind) { //myChart.data.datasets[3].data[counterfen].y = cpt_ce[counterfen][0] + cpt_ce[counterfen][1] + cpt_ce[counterfen][2] + cpt_ce[counterfen][3]; } //find beginning of elem of chart of working clock - chartBeginIndex = myChart.data.datasets[ind*2+2].data.findIndex((element)=>element.x>=working_clock/60); + chartBeginIndex = myChart.data.datasets[ind*2+2].data.findIndex((element)=>element.x>=working_clock/60) ; for (counterfen=chartBeginIndex; counterfen elem[0]==input_uid); + if (tempFileIndex != -1) { + parseobject(null,true,importDataArray[tempFileIndex][1]); + } + } else { + parseobject(input_uid); + } var timestamp_lastdatadate = new Date(input_uid*1000 + time_in_s*1000); var timestamp_lastdatatime = timestamp_lastdatadate.toLocaleDateString() + " " + timestamp_lastdatadate.toLocaleTimeString(); setTimeout(function() { @@ -8134,6 +8164,7 @@ function reanimate(arg_time) { //copy from common start calls document.getElementById("top_subtitle").classList.add("topClose"); document.getElementById("top_title").classList.add("topOpen"); + document.getElementById("expandbutton").style.display = "block"; if (complex_mode == 0) { var argument = object.P_hx[0][0]; // 0 is manual @@ -9316,13 +9347,46 @@ function loadobject(input_uid) { } function load() { + toLoadTransition(); +} + + +function loadSourceExt() { + document.getElementById("rescuebuttons").style.display = "none"; + document.getElementById("modalLoadHugeButtons").style.display = "none"; + document.getElementById("modalLoadImportButtons").style.display = "block"; + document.getElementById("fileselection").style.display = "block"; + document.getElementById("modalLoadDescription").style.display = "inline"; + document.getElementById("modalLoadDescription").innerHTML = "Import from external .CSV file:"; + document.getElementById("loadfile_container").classList.add("collapse"); + document.getElementById("loadfile_container").classList.remove("compress"); + document.getElementById("loadfile_container").style.display = "block"; + document.getElementById("loadfile_container").innerHTML = + ` +
After loading, the database of external sim-files will appear here.
+ ` + //document.getElementById("loadfile_container").style.display = "block"; + //importDialog(); +} + +function loadSourceLocal() { if ((localStorage.getItem("keys") == null) || (JSON.parse(localStorage.getItem("keys")).length == 0)) { displayWarning("No saved data", "No previously saved sim-file data.") } else { generateFileKeys(); - toLoadTransition(); document.getElementById("rescuebuttons").style.display = "none"; + document.getElementById("modalLoadHugeButtons").style.display = "none"; + document.getElementById("fileselection").style.display = "none"; + document.getElementById("modalLoadNormalButtons").style.display = "block"; + document.getElementById("modalLoadDescription").style.display = "inline"; + document.getElementById("loadfile_container").classList.remove("collapse"); + document.getElementById("loadfile_container").classList.remove("compress"); + document.getElementById("loadfile_container").style.display = "block"; + + importDataArray.length = 0; + } + } function rescue() { @@ -9347,10 +9411,25 @@ function generateFileKeys() { } function renderImportList() { - let El1 = document.getElementById("filecontent"); - for (impcountr = 0; impcountr=0; i--) { tempObject = loadobject(inputkeysarray[i]); @@ -9486,13 +9565,13 @@ function renderFileList(inputkeysarray) { El2.setAttribute('data-duration', tempDuration); if (isComplex == 0) { if (tempObject.name != "") { - El2.setAttribute('style', 'height:88px'); + El2.setAttribute('style', 'height:90px'); } } else { if (tempObject.name != "") { - El2.setAttribute('style', 'height:104px'); + El2.setAttribute('style', 'height:107px'); } else { - El2.setAttribute('style', 'height:88px'); + El2.setAttribute('style', 'height:90px'); } } if (isComplex == 0) { @@ -9634,9 +9713,27 @@ async function downloadExcel() { } */ +let inputname = ""; + function exportFunction() { - if (confirm("This will export all the Sim-Files on device to a .csv (comma-separated values) file. You can then open this file in Excel or other spreadsheet application.")) { - exportKeys(); + //reset inputname + inputname = ""; + testKeys = JSON.parse(localStorage.getItem("keys")); + if (testKeys.length>=1) { + displayWarning("Export sim-files to CSV", + ` +
This will export all ${testKeys.length} sim-files on device to a .csv (comma-separated values) file. You can then open this file in Excel or other spreadsheet application. You may also import this database file into SimTIVA for viewing.
+
+
+
+
+ Save as file name:
+ .csv +
+
+
+
ProceedCancel
+ `) } } @@ -9697,80 +9794,114 @@ function exportDataFile(input_uid) { } } -function exportKeys() { - testKeys = JSON.parse(localStorage.getItem("keys")); +function exportKeys(filenameentry,testKeys) { + testString = "UID,Timestamp,Duration,Name,Age,Sex,BW,BH,Mode,Model description,Weblink\n"; for (kc = 0; kc < testKeys.length; kc++) { temp = exportDataFile(testKeys[kc]); if (temp != undefined) testString += temp; } - exportGenerateDownload(testString); -} - -function importDialog() { - displayWarning("Import",` -
-

- `); + exportGenerateDownload(testString,filenameentry); } let importDataArray = new Array(); function previewFile() { - const content = document.getElementById("filecontent"); + const content = document.getElementById("loadfile_container"); const [file] = document.getElementById("myFile").files; const reader = new FileReader(); reader.addEventListener( "load", () => { - errorCount = 0; //crosschecking code to see whether file type is correct - //... - //iterate through CSV file - let parseArrayRaw = reader.result.split("\n"); - let impcount; - for (impcount = 1; impcount"; + console.log(errorMessage); + } + if (errorMessage != "") { + let tempRaw = reader.result.split("\n"); //last record is 2nd last line. last line is an empty line. + + if (tempRaw != undefined) { + if (reader.result.slice(0,26) != "UID,Timestamp,Duration,Nam") { + errorMessage += "Cannot read file. Does not seem to be a SimTIVA .csv database file." + "
"; + } else { + //preview first record and see if it's fine + let parseArrayComma = parseArrayRaw[1].split(","); + let parseArrayURL = parseArrayComma[parseArrayComma.length-1]; + if (parseArrayURL.slice(0,19) != "https://simtiva.app") { + errorMessage += "Wrong weblink reference. Possible corrupted database."; + } + } } else { - errorCount += 1; + errorMessage += "Cannot read file. Does not seem to be a SimTIVA .csv database file."; } - console.log(obj); - console.log(errorCount); } - impcount -= 1; - alert("Total " + impcount + " records retrieved.\n" + - importDataArray.length + " records read successfully.\n" + - errorCount + " records failed to load."); - renderImportList(); - //console.log("read successfully"); - //console.log(parseArrayObj); - + console.log(errorMessage); + if (errorMessage != "") { + displayWarning("Failed import",errorMessage); + } else { + errorCount = 0; + + //iterate through CSV file + let parseArrayRaw = reader.result.split("\n"); + let impcount; + //pop importDataArray + importDataArray.length = 0; + for (impcount = 1; impcount" + + importDataArray.length + " records read successfully.
" + + errorCount + " records failed to load."); + document.getElementById("btn_load_import").classList.remove("disabled"); + document.getElementById("loadfile_container").classList.remove("collapse"); + document.getElementById("loadfile_container").classList.add("compress"); + document.getElementById("loadfile_container").innerHTML = ''; + renderImportList(); + } }, false, ); if (file) { reader.readAsText(file); + reader.fileName = file.name; } } -function exportGenerateDownload(input_string) { +function exportGenerateDownload(input_string,expFileName) { jsonObject = "data:text/csv;charset=utf-8," + encodeURIComponent(input_string); - + if (expFileName.length>0) { + expFileName += ".csv"; + } else { + expFileName = "export.csv"; + } const link2 = document.createElement('a'); link2.target = "_blank"; - link2.download = 'export.csv'; + link2.download = expFileName; link2.href = jsonObject; link2.click(); link2.delete; + + document.getElementById("fileexportblock").innerHTML = "File export complete. You may close this window. Click here if you can't find the file in your downloads folder."; + herelink.target="_blank"; + herelink.download = expFileName; + herelink.href = jsonObject; + document.getElementById("btn_exportProceed").classList.add("disabled"); } @@ -9780,9 +9911,7 @@ function selectFileBox(input_uid) { for (i=0; i=1)) position = 0; + if (position<=0) position = 0; + if (position>=1) position = 1; gradientBgRed.addColorStop(0, 'rgba(231,50,39,0.4)'); gradientBgRed.addColorStop(position, 'rgba(231,50,39,0.4)'); gradientBgRed.addColorStop(position, 'rgba(231,50,39,0.1'); @@ -10889,7 +11019,8 @@ function parsedisplay(t,sex,model,VI,d,mode) { const gradientBgYellow = ctx.createLinearGradient(chartArea.left, 0, chartArea.right, 0); var position = (scales.x.getPixelForValue(time_in_s/60) - scales.x.getPixelForValue(scales.x.min)) / chartArea.width; - if ((position<=0) || (position>=1)) position = 0; + if (position<=0) position = 0; + if (position>=1) position = 1; gradientBgYellow.addColorStop(0, yellowPri50); gradientBgYellow.addColorStop(position, yellowPri50); gradientBgYellow.addColorStop(position, yellowSec10); @@ -10902,7 +11033,8 @@ function parsedisplay(t,sex,model,VI,d,mode) { const gradientBgBlue = ctx.createLinearGradient(chartArea.left, 0, chartArea.right, 0); var position = (scales.x.getPixelForValue(time_in_s/60) - scales.x.getPixelForValue(scales.x.min)) / chartArea.width; - if ((position<=0) || (position>=1)) position = 0; + if (position<=0) position = 0; + if (position>=1) position = 1; gradientBgBlue.addColorStop(0, blueLight50); gradientBgBlue.addColorStop(position, blueLight50); gradientBgBlue.addColorStop(position, blueSec10); @@ -10915,7 +11047,8 @@ function parsedisplay(t,sex,model,VI,d,mode) { const gradientBgGreen = ctx.createLinearGradient(chartArea.left, 0, chartArea.right, 0); var position = (scales.x.getPixelForValue(time_in_s/60) - scales.x.getPixelForValue(scales.x.min)) / chartArea.width; - if ((position<=0) || (position>=1)) position = 0; + if (position<=0) position = 0; + if (position>=1) position = 1; gradientBgGreen.addColorStop(0, 'rgba(9,203,93,0.7)'); gradientBgGreen.addColorStop(position, 'rgba(9,203,93,0.7)'); gradientBgGreen.addColorStop(position, 'rgba(9,203,93,0.1'); @@ -12581,10 +12714,12 @@ function preparerange() { El0.max = current_time; El0.step = 5; } + El1.min = Math.floor(current_time/5)*5; //round down to nearest 5 El1.max = Math.floor(max_time/10)*10 - 10; + El0.dataMax = xmin+5; - El1.dataMin = xmax-5; + El1.dataMin = xmax-10; El0.value = myChart.scales.x.min; El1.value = myChart.scales.x.max; diff --git a/styles.css b/styles.css index c0399cf..b8b8518 100644 --- a/styles.css +++ b/styles.css @@ -1584,6 +1584,7 @@ a.button.disabled:hover, a.button.disabled:active { color:rgb(190,190,190); background-color:rgb(240,240,240); cursor:not-allowed; + pointer-events: none; } a.button.tinybutton { @@ -2182,7 +2183,7 @@ body.dark .loadduration { font-size:40px; } #previewright { - padding-left:9px; + padding-left:8px; flex-basis:calc(100% - 40px); } #prompt { @@ -4251,7 +4252,25 @@ body.dark #progressbar { border-color:#ddd; } +#modalLoadDescription { + display: none; +} +#modalLoadNormalButtons { + display: none; +} + +#modalLoadImportButtons { + display: none; + padding-bottom: 15px; +} + +#fileselection { + display: none; + padding-top: 6px; + padding-bottom: 6px; + +} #loadfile_container { width: 100%; @@ -4260,6 +4279,20 @@ body.dark #progressbar { margin-bottom: 10px; overflow-y: auto; height: 60dvh; + display: none; + /*transition: all ease 1s;*/ +} + +#loadfile_container.collapse { + height: 200px; + padding-top: 70px; + width: 70%; + margin-left: auto; + margin-right: auto; +} + +#loadfile_container.compress { + height: 50dvh; } #filecontent { @@ -4272,8 +4305,6 @@ body.dark #progressbar { float: right; font-size: 150%; position: relative; - top: -5px; - color: rgba(128,128,128,0.8); cursor: pointer; } @@ -4302,7 +4333,7 @@ body.dark #progressbar { background: transparent; border: 2px solid rgb(220,220,220); display: flex; - padding: 0.5rem 3px 0.5rem 3px; + padding: 0.4rem 3px 0.5rem 3px; margin-bottom: 0.5rem; height: 72px; transform: scale(1); diff --git a/view.html b/view.html index 3565f13..185c3ad 100644 --- a/view.html +++ b/view.html @@ -424,25 +424,31 @@ - @@ -773,6 +800,9 @@ +
+ +