Merge branch 'feature/ui-modernization' into master
No files matched your search
@@ -2,8 +2,8 @@ cff-version: 1.2.0
|
||||
message: "If you use this software, please cite the TRIMSP-NL Workbench publication and this software release."
|
||||
title: "TRIMSP-NL Workbench"
|
||||
type: software
|
||||
version: 1.4.0
|
||||
date-released: 2026-08-27
|
||||
version: 1.5.0
|
||||
date-released: 2026-08-28
|
||||
url: "http://musruser.psi.ch/TRIMSP-NL/"
|
||||
repository-code: "https://gitea.psi.ch/LMU/TRIMSP"
|
||||
license: GPL-3.0-or-later
|
||||
|
||||
@@ -118,6 +118,11 @@ npm start
|
||||
Note: you do not need to run `npm install` every time.
|
||||
Next time you can simply run `npm start` only.
|
||||
|
||||
Use the `?` button for contextual Quick Start help. The adjacent share button
|
||||
copies a link containing the current scientific configuration for the web
|
||||
application. Shared links include layers, projectile, scan, RNG, and advanced
|
||||
settings, but exclude local paths, file prefixes, worker counts, and results.
|
||||
|
||||
###### Building RPM and DEB packages ######
|
||||
|
||||
Install [Node.js] (`nodejs`, `nodejs-libs` and `npm`), plus the Fortran toolchain
|
||||
@@ -228,24 +233,56 @@ which matches how the packaged app currently handles simulation input/output.
|
||||
|
||||
###### Deploying on a web server ######
|
||||
|
||||
The browser version requires a server-side setup in addition to the static
|
||||
HTML/JavaScript files.
|
||||
The repository includes the browser assets, CGI runner, and a deployment
|
||||
script for an Apache-style server. With the default settings, run:
|
||||
|
||||
In particular:
|
||||
```bash
|
||||
sudo ./scripts/deploy-web.sh
|
||||
```
|
||||
|
||||
* the browser build uses `TrimSPweb.js` rather than the Electron-specific
|
||||
`TrimSPelec.js`
|
||||
* the simulation backend must be available on the server
|
||||
* a web-exposed helper/CGI layer is required to create input files, run the
|
||||
backend, and return the generated output
|
||||
The script expects the TRIM.SP-NL engine to be installed already and installs:
|
||||
|
||||
Notes:
|
||||
```text
|
||||
/var/www/html/TRIMSP-NL/ browser application
|
||||
/var/www/cgi-bin/singleTrimSP.cgi CGI runner
|
||||
```
|
||||
|
||||
* this repository does not currently include the CGI/backend helper used by
|
||||
the online deployment
|
||||
* a plain static web server is therefore not sufficient for running web
|
||||
simulations from this checkout
|
||||
* static hosting can still be used to test page loading only
|
||||
By default, the CGI uses the existing executable at
|
||||
`/usr/local/bin/trimspNL`. Set `TRIMSP_BIN_PATH` if it is installed elsewhere.
|
||||
|
||||
Deployment paths and ownership can be changed through environment variables:
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
| --- | --- | --- |
|
||||
| `TRIMSP_WEB_TARGET` | `/var/www/html/TRIMSP-NL` | Browser asset directory |
|
||||
| `TRIMSP_CGI_TARGET` | `/var/www/cgi-bin/singleTrimSP.cgi` | Installed CGI path |
|
||||
| `TRIMSP_WEB_OWNER` | `apache` | Web file owner |
|
||||
| `TRIMSP_WEB_GROUP` | `apache` | Web file group |
|
||||
| `TRIMSP_BIN_PATH` | `/usr/local/bin/trimspNL` | Existing engine executable used by the CGI |
|
||||
|
||||
For example, to use an engine installed in `/opt/trimsp/bin`:
|
||||
|
||||
```bash
|
||||
sudo TRIMSP_BIN_PATH=/opt/trimsp/bin/trimspNL \
|
||||
./scripts/deploy-web.sh
|
||||
```
|
||||
|
||||
The web server must execute the CGI through `/cgi-bin/singleTrimSP.cgi`. The
|
||||
CGI keeps simulation files in filesystem `/tmp` and serves only requested run
|
||||
outputs and archives; `/tmp` does not need to be exposed through the web
|
||||
server. On SELinux systems, inspect denied operations with:
|
||||
|
||||
```bash
|
||||
ausearch -m avc -ts recent
|
||||
```
|
||||
|
||||
The Apache account needs permission to execute the CGI and engine and to create
|
||||
the per-run directories in `/tmp`.
|
||||
|
||||
After a web run, the status bar provides the results archive through
|
||||
**Outputs** and shows CGI status plus captured TRIM.SP-NL output through
|
||||
**Details**. Diagnostic output is kept in the browser session only and is not
|
||||
included in the `.tgz` archive.
|
||||
|
||||
### Contact ###
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="ZGUI.css">
|
||||
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
|
||||
<script src="TrimSPweb.js"></script>
|
||||
<script>
|
||||
// Electron exposes CommonJS module support; plain browsers do not.
|
||||
@@ -16,104 +16,134 @@
|
||||
</script>
|
||||
<script src="TrimSPlib.js"></script>
|
||||
<script src="myplots.js"></script>
|
||||
<!script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
|
||||
<title>TRIMSP-NL Workbench</title>
|
||||
</head>
|
||||
<body onresize="resizePl()" onload="adjust_table();adjust_scans();">
|
||||
<table style="width: 100%;">
|
||||
<tr><td>
|
||||
<div class="col-full">
|
||||
<div class="tab">
|
||||
<button class="tablinks" onclick="openTab(event,'Layers')" id="btnLayers">Layers</button>
|
||||
<button class="tablinks" onclick="openTab(event,'Scans')" id="btnScans">Scans</button>
|
||||
<button class="tablinks" onclick="openTab(event,'Plots')" id="btnPlots">Plots</button>
|
||||
<button class="tablinks" onclick="openTab(event,'Other')" id="btnOther">Other Parameters</button>
|
||||
<main class="app-shell">
|
||||
<section class="app-card">
|
||||
<nav class="tab" aria-label="Simulation workspace">
|
||||
<button type="button" class="tablinks" onclick="openTab(event,'Layers')" id="btnLayers">Layers</button>
|
||||
<button type="button" class="tablinks" onclick="openTab(event,'Scans')" id="btnScans">Scan</button>
|
||||
<button type="button" class="tablinks" onclick="openTab(event,'Plots')" id="btnPlots">Results</button>
|
||||
<button type="button" class="tablinks" onclick="openTab(event,'Other')" id="btnOther">Advanced</button>
|
||||
<button type="button" class="help-button" onclick="showQuickStartHelp();"
|
||||
title="Quick Start / Help" aria-label="Quick Start / Help">?</button>
|
||||
<button type="button" class="help-button share-button" id="shareConfigButton"
|
||||
onclick="copyWebConfigurationLink();" title="Copy configuration link"
|
||||
aria-label="Copy configuration link">
|
||||
<svg class="share-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<circle cx="18" cy="5" r="2.5"></circle>
|
||||
<circle cx="6" cy="12" r="2.5"></circle>
|
||||
<circle cx="18" cy="19" r="2.5"></circle>
|
||||
<path d="M8.2 10.8 15.8 6.2M8.2 13.2l7.6 4.6"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</nav>
|
||||
<div class="main-action-bar" role="toolbar" aria-label="Simulation actions">
|
||||
<div class="action-bar-title">
|
||||
<img class="app-brand-icon" src="icon.png" alt="" aria-hidden="true">
|
||||
<div>
|
||||
<span class="action-bar-eyebrow">TRIMSP-NL Workbench</span>
|
||||
<strong>Low-energy implantation simulation</strong>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="run-action" id="runSimulationButton" onclick="startSim();">Run simulation</button>
|
||||
</div>
|
||||
<div id="Layers" class="tabcontent">
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
<td style="vertical-align: top;">
|
||||
<table cellpadding="5">
|
||||
<tr id="FileRow">
|
||||
<td><label>File name prefix:</label></td>
|
||||
<td>
|
||||
<input name="fileNamePrefix" id="fileNamePrefix" type="text" style="width:70%" value="SrTiO3"/>
|
||||
<input name="trimPath" id="trimPath" type="hidden"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="FolderRow">
|
||||
<td><label>Save folder:</label></td>
|
||||
<td>
|
||||
<input type="text" disabled style="width:70%" id="workPath" name="workPath" title="Use Browse to select the save folder" />
|
||||
<input type="button" value="Browse"
|
||||
id="browseFolde" onclick="browseForFolder();">
|
||||
<input type="file" id="folderPicker" style="display:none" webkitdirectory directory>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label>Number of Layers:</label></td>
|
||||
<td><input name="numLayer" id="numLayer" type="number" size="3" step="1" min="1" max="100" value="1" onchange="adjust_table()"></td>
|
||||
</tr>
|
||||
<tr><td colspan="2">
|
||||
<table id="LTable" border="2" cellpadding="10">
|
||||
<tr><td><b>Layer #</b></td><td><b>Composition</b></td><td><b>Density [g/cm<sup>3</sup>]</b></td><td><b>Thickness [Å]</b></td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
<div class="output-settings">
|
||||
<label class="compact-field output-prefix" id="FileRow" for="fileNamePrefix">
|
||||
<span>File prefix</span>
|
||||
<input name="fileNamePrefix" id="fileNamePrefix" type="text" value="SrTiO3">
|
||||
<input name="trimPath" id="trimPath" type="hidden">
|
||||
</label>
|
||||
<div class="compact-field output-folder" id="FolderRow">
|
||||
<label for="workPath">Save folder</label>
|
||||
<div class="field-with-action">
|
||||
<input type="text" disabled id="workPath" name="workPath" title="Use Browse to select the save folder">
|
||||
<input type="button" value="Browse" id="browseFolde" onclick="browseForFolder();">
|
||||
<input type="file" id="folderPicker" hidden webkitdirectory directory>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layers-workspace">
|
||||
<section class="settings-panel target-panel" aria-labelledby="targetPanelTitle">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<span class="section-eyebrow">Target</span>
|
||||
<h2 id="targetPanelTitle">Layer stack</h2>
|
||||
</div>
|
||||
<label class="layer-count" for="numLayer">
|
||||
<span>Layers</span>
|
||||
<input name="numLayer" id="numLayer" type="number" step="1" min="1" max="100" value="1" onchange="adjust_table()">
|
||||
</label>
|
||||
</div>
|
||||
<div class="layer-table-wrap">
|
||||
<table id="LTable">
|
||||
<tr><td><b>Layer</b></td><td><b>Composition</b></td><td><b>Density [g/cm<sup>3</sup>]</b></td><td><b>Thickness [Å]</b></td></tr>
|
||||
</table>
|
||||
</td>
|
||||
<td style="vertical-align: top;">
|
||||
<table cellpadding="5">
|
||||
<tr><td collspan="2"><b>Projectile parameters</b></td></tr>
|
||||
<tr><td>Projectile</td>
|
||||
<td><select name="ProjType" id="ProjType" onchange="ProjSmartDefaults()" onload="ProjSmartDefaults()">
|
||||
<option selected="selected" value="muon">muon</option>
|
||||
<option value="Li-8">Li-8</option>
|
||||
<option value="Be-11">Be-11</option>
|
||||
<option value="B-12">B-12</option>
|
||||
<option value="Mg-31">Mg-31</option>
|
||||
<option value="H">H</option>
|
||||
<option value="He">He</option>
|
||||
<option value="Ar">Ar</option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td>Number of projectiles</td>
|
||||
<td><input name="numberProj" id="numberProj" type="text" size="7" value="1000" onchange="//ProjNumberLimit()"></td>
|
||||
</tr>
|
||||
<tr><td>Starting depth [Å]</td><td><input name="z0" id="z0" type="text" size="7" value="0"></td></tr>
|
||||
<tr><td>Depth increment [Å]</td><td><input name="dz" id="dz" type="text" size="7" value="20"></td></tr>
|
||||
<tr><td>Energy [eV]</td><td><input name="valEnergy" id="valEnergy" type="text" size="7" value="2000"></td></tr>
|
||||
<tr><td>Energy sigma [eV]</td><td><input name="sigEnergy" id="sigEnergy" type="text" size="7" value="450"></td></tr>
|
||||
<tr><td>Angle [deg]</td><td><input name="valAngle" id="valAngle" type="text" size="7" value="0"></td></tr>
|
||||
<tr><td>Angle sigma [deg]</td><td><input name="sigAngle" id="sigAngle" type="text" size="7" value="15"></td></tr>
|
||||
<tr>
|
||||
<td>RNG / Seed</td>
|
||||
<td>
|
||||
<select name="rngType" id="rngType">
|
||||
<option value="pcg32" selected="selected">PCG32</option>
|
||||
<option value="ranlux">RANLUX</option>
|
||||
<option value="xoshiro">xoshiro256**</option>
|
||||
</select>
|
||||
<input name="ranSeed" id="ranSeed" type="text" size="7" value="78741">
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td><button onclick="startSim();">Start</button></td><td></td></tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
<section class="settings-panel projectile-panel" aria-labelledby="projectilePanelTitle">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<span class="section-eyebrow">Beam</span>
|
||||
<h2 id="projectilePanelTitle">Projectile</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="projectile-grid">
|
||||
<label class="settings-field" for="ProjType"><span>Projectile</span>
|
||||
<select name="ProjType" id="ProjType" onchange="ProjSmartDefaults()">
|
||||
<option selected="selected" value="muon">muon</option>
|
||||
<option value="Li-8">Li-8</option>
|
||||
<option value="Be-11">Be-11</option>
|
||||
<option value="B-12">B-12</option>
|
||||
<option value="Mg-31">Mg-31</option>
|
||||
<option value="H">H</option>
|
||||
<option value="He">He</option>
|
||||
<option value="Ar">Ar</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="settings-field" for="numberProj"><span>Projectiles</span><input name="numberProj" id="numberProj" type="text" value="1000"></label>
|
||||
<label class="settings-field" for="valEnergy"><span>Energy [eV]</span><input name="valEnergy" id="valEnergy" type="text" value="2000"></label>
|
||||
<label class="settings-field" for="sigEnergy"><span>Energy sigma [eV]</span><input name="sigEnergy" id="sigEnergy" type="text" value="450"></label>
|
||||
<label class="settings-field" for="valAngle"><span>Angle [deg]</span><input name="valAngle" id="valAngle" type="text" value="0"></label>
|
||||
<label class="settings-field" for="sigAngle"><span>Angle sigma [deg]</span><input name="sigAngle" id="sigAngle" type="text" value="15"></label>
|
||||
<label class="settings-field" for="z0"><span>Starting depth [Å]</span><input name="z0" id="z0" type="text" value="0"></label>
|
||||
<label class="settings-field" for="dz"><span>Depth increment [Å]</span><input name="dz" id="dz" type="text" value="20"></label>
|
||||
<label class="settings-field" for="rngType"><span>RNG</span>
|
||||
<select name="rngType" id="rngType">
|
||||
<option value="pcg32" selected="selected">PCG32</option>
|
||||
<option value="ranlux">RANLUX</option>
|
||||
<option value="xoshiro">xoshiro256**</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="settings-field" for="ranSeed"><span>Seed</span><input name="ranSeed" id="ranSeed" type="text" value="78741"></label>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<div id="Scans" class="tabcontent">
|
||||
<input type="checkbox" name="scanSeq" id="scanSeq" onChange="adjust_scans();"> Enable scan on
|
||||
<select name="scanType" id="scanType" onchange="adjust_scans();">
|
||||
<option value="scanLoop">Loop</option>
|
||||
<option value="scanVals">Set of values</option>
|
||||
</select>
|
||||
<div id="ScansControls" style="visibility: hidden;">
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
<td colspan="1">Scan:
|
||||
<select name="comboScan" id="comboScan" onchange="">
|
||||
<section class="settings-panel scan-panel" aria-labelledby="scanPanelTitle">
|
||||
<div class="scan-heading">
|
||||
<div>
|
||||
<span class="section-eyebrow">Execution plan</span>
|
||||
<h2 id="scanPanelTitle">Parameter scan</h2>
|
||||
</div>
|
||||
<label class="scan-enable" for="scanSeq">
|
||||
<input type="checkbox" name="scanSeq" id="scanSeq" onchange="adjust_scans();">
|
||||
<span>Enable scan</span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="ScansControls" hidden>
|
||||
<div class="scan-controls-grid">
|
||||
<label class="settings-field" for="scanType"><span>Sequence</span>
|
||||
<select name="scanType" id="scanType" onchange="adjust_scans();">
|
||||
<option value="scanLoop">Range</option>
|
||||
<option value="scanVals">Set of values</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="settings-field" for="comboScan"><span>Parameter</span>
|
||||
<select name="comboScan" id="comboScan" onchange="updateScanSummary();">
|
||||
<option value="EScan">Energy [eV]</option>
|
||||
<option value="SigEScan">Energy sigma [eV]</option>
|
||||
<option value="AngleScan">Angle [deg]</option>
|
||||
@@ -121,44 +151,44 @@
|
||||
<option value="NProjScan">Number of projectiles</option>
|
||||
<option value="dScan">Thickness of layer # 1</option>
|
||||
</select>
|
||||
</td>
|
||||
<td colspan="1">
|
||||
Threads:
|
||||
<input name="scanThreads" id="scanThreads" type="number" min="1" step="1" value="" onchange="adjust_scans();">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div id="ScansLine" style="visibility: hidden;">
|
||||
<br>Set of values: <input name="scanList" id="scanList" type="text" value="1000">
|
||||
Corresponding depth increment: <input name="scanListdz" id="scanListdz" type="text" value="">
|
||||
</div>
|
||||
<table id="ScansTable" style="width: 100%;visibility: hidden;">
|
||||
<tr>
|
||||
<td>
|
||||
<!input type="radio" name="ScanType" value="Loop" checked onChange="">
|
||||
<!input type="radio" name="ScanType" value="ScanList" onChange="">
|
||||
From: <input name="scanFrom" id="scanFrom" type="text" value="1000">
|
||||
</td>
|
||||
<td>
|
||||
Step: <input name="scanStep" id="scanStep" type="text" value="2000">
|
||||
</td>
|
||||
<td>
|
||||
To: <input name="scanTo" id="scanTo" type="text" value="14000">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</label>
|
||||
<label class="settings-field" for="scanThreads"><span>Workers</span>
|
||||
<input name="scanThreads" id="scanThreads" type="number" min="1" step="1" value="" oninput="adjust_scans();">
|
||||
</label>
|
||||
</div>
|
||||
<div id="ScansTable" class="scan-values-panel scan-range-grid">
|
||||
<label class="settings-field" for="scanFrom"><span>From</span><input name="scanFrom" id="scanFrom" type="text" value="1000" oninput="updateScanSummary();"></label>
|
||||
<label class="settings-field" for="scanStep"><span>Step</span><input name="scanStep" id="scanStep" type="text" value="2000" oninput="updateScanSummary();"></label>
|
||||
<label class="settings-field" for="scanTo"><span>To</span><input name="scanTo" id="scanTo" type="text" value="14000" oninput="updateScanSummary();"></label>
|
||||
</div>
|
||||
<div id="ScansLine" class="scan-values-panel scan-list-grid" hidden>
|
||||
<label class="settings-field" for="scanList"><span>Values, separated by commas</span><input name="scanList" id="scanList" type="text" value="1000" oninput="updateScanSummary();"></label>
|
||||
<label class="settings-field" for="scanListdz"><span>Matching depth increments (optional)</span><input name="scanListdz" id="scanListdz" type="text" value="" oninput="updateScanSummary();"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="scanSummary" class="scan-summary" role="status" aria-live="polite">Single simulation using the Layers settings.</div>
|
||||
</section>
|
||||
</div>
|
||||
<div id="Plots" class="tabcontent">
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
<td align="center"><div id="plotRge"><!-- Plotly chart will be drawn inside this DIV --></div></td>
|
||||
<td align="center"><div id="plotFrac"><!-- Plotly chart will be drawn inside this DIV --></div></td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="results-grid">
|
||||
<section class="result-plot-panel" aria-label="Implantation profile plot">
|
||||
<div id="plotRge" class="result-plot"><!-- Plotly chart will be drawn inside this DIV --></div>
|
||||
</section>
|
||||
<section class="result-plot-panel" aria-label="Stopping fraction plot">
|
||||
<div id="plotFrac" class="result-plot"><!-- Plotly chart will be drawn inside this DIV --></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<div id="Other" class="tabcontent">
|
||||
<table cellpadding="5">
|
||||
<div class="advanced-layout">
|
||||
<section class="settings-panel advanced-parameters" aria-labelledby="advancedParametersTitle">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<span class="section-eyebrow">Transport model</span>
|
||||
<h2 id="advancedParametersTitle">Advanced parameters</h2>
|
||||
</div>
|
||||
</div>
|
||||
<table>
|
||||
<tr>
|
||||
<td class="tooltip">
|
||||
EF
|
||||
@@ -373,29 +403,43 @@
|
||||
<input name="parIRL" id="parIRL" type="number" step="1" min="0" max="1" value="0">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p>
|
||||
For further details see:<br>
|
||||
W. Eckstein, <i>Computer Simulation of Ion-Solid Interactions</i>,<br>
|
||||
Springer Series in Materials Science, Vol. 10 (Springer-Verlag, Berlin, 1991).<br>
|
||||
<a href="https://doi.org/10.1007/978-3-642-73513-4">
|
||||
https://doi.org/10.1007/978-3-642-73513-4
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
If you use this software in published work, please cite the relevant
|
||||
TRIM.SP references and the TRIMSP-NL Workbench paper.
|
||||
</p>
|
||||
<p>
|
||||
Source repository:
|
||||
<a href="https://gitea.psi.ch/LMU/TRIMSP">
|
||||
gitea.psi.ch/LMU/TRIMSP
|
||||
</a>
|
||||
</p>
|
||||
</table>
|
||||
</section>
|
||||
<aside class="reference-panel" aria-labelledby="referencePanelTitle">
|
||||
<span class="section-eyebrow">Documentation</span>
|
||||
<h2 id="referencePanelTitle">Reference & citation</h2>
|
||||
<p>
|
||||
For model details, see W. Eckstein,
|
||||
<i>Computer Simulation of Ion-Solid Interactions</i>, Springer Series
|
||||
in Materials Science, Vol. 10 (Springer-Verlag, Berlin, 1991).
|
||||
</p>
|
||||
<a class="reference-link" href="https://doi.org/10.1007/978-3-642-73513-4">
|
||||
doi.org/10.1007/978-3-642-73513-4
|
||||
</a>
|
||||
<div class="reference-divider"></div>
|
||||
<span class="reference-label">TRIMSP-NL Workbench</span>
|
||||
<p>
|
||||
Z. Salman, R. M. L. McFadden, and T. Prokscha,
|
||||
<i>Modernization and Statistical Validation of a Multilayer TRIM.SP
|
||||
Code for Low-Energy Muon and Ion Implantation</i> (2026).
|
||||
</p>
|
||||
<a class="reference-link" href="https://doi.org/10.48550/arXiv.2609.00094">
|
||||
doi.org/10.48550/arXiv.2609.00094
|
||||
</a>
|
||||
<p>
|
||||
If you use this software in published work, please cite the relevant
|
||||
TRIM.SP references and the TRIMSP-NL Workbench paper.
|
||||
</p>
|
||||
<div class="reference-divider"></div>
|
||||
<span class="reference-label">Source repository</span>
|
||||
<a class="reference-link" href="https://gitea.psi.ch/LMU/TRIMSP">
|
||||
gitea.psi.ch/LMU/TRIMSP
|
||||
</a>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td></tr>
|
||||
</table>
|
||||
</section>
|
||||
</main>
|
||||
<div id="statusBar" class="status-bar">
|
||||
<span class="status-item"><strong>Folder:</strong> <span id="statusWorkPath">-</span></span>
|
||||
<span class="status-item"><strong>Backend:</strong> <span id="statusBackend">-</span></span>
|
||||
@@ -406,7 +450,29 @@
|
||||
<div id="pBar" class="status-progress-fill">0%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="webStatusActions" class="status-actions" hidden>
|
||||
<a id="statusOutputLink" class="status-action-link" href="#" hidden>Outputs</a>
|
||||
<button type="button" class="status-details-button" onclick="openWebRunDetails();">Details</button>
|
||||
</div>
|
||||
</div>
|
||||
<dialog id="runDetailsDialog" class="run-details-dialog">
|
||||
<div class="run-details-heading">
|
||||
<div>
|
||||
<span class="section-eyebrow">Web backend</span>
|
||||
<h2>Run details</h2>
|
||||
</div>
|
||||
<button type="button" onclick="document.getElementById('runDetailsDialog').close();">Close</button>
|
||||
</div>
|
||||
<p class="run-details-note">CGI responses and TRIM.SP-NL output for this browser session.</p>
|
||||
<pre id="webRunLog" class="run-details-log">No web requests have been made yet.</pre>
|
||||
</dialog>
|
||||
<dialog id="quickStartDialog" class="quick-start-dialog" aria-label="TRIMSP-NL Quick Start" aria-modal="false">
|
||||
<div class="quick-start-dialog-header quick-start-drag-handle">
|
||||
<strong>TRIMSP-NL Quick Start</strong>
|
||||
<button type="button" onclick="closeQuickStartHelp();" aria-label="Close Quick Start">Close</button>
|
||||
</div>
|
||||
<iframe id="quickStartFrame" class="quick-start-frame" title="TRIMSP-NL Quick Start"></iframe>
|
||||
</dialog>
|
||||
</body>
|
||||
<script>
|
||||
|
||||
@@ -429,12 +495,9 @@
|
||||
if (webOrApp) {
|
||||
console.log('this script is not running in Node.js');
|
||||
// Run as a web app
|
||||
//let row = document.getElementById("FileRow");
|
||||
//row.parentNode.removeChild(row);
|
||||
//row = document.getElementById("FolderRow");
|
||||
//row.parentNode.removeChild(row);
|
||||
document.getElementById("FileRow").style.visibility = "hidden";;
|
||||
document.getElementById("FolderRow").style.visibility = "hidden";;
|
||||
document.getElementById("FileRow").hidden = true;
|
||||
document.getElementById("FolderRow").hidden = true;
|
||||
document.getElementById("webStatusActions").hidden = false;
|
||||
} else {
|
||||
console.log('this script is running in Node.js');
|
||||
// Run as standalone app
|
||||
|
||||
@@ -8,6 +8,50 @@ const { execSync, execFile } = require('child_process');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
// Renderer requests for native auxiliary windows stay in the main process.
|
||||
function openElectronAuxiliaryWindow(kind, options = {}) {
|
||||
if (kind !== 'quickStart') { return false; }
|
||||
ipcRenderer.send('openQuickStartWindow', options || {});
|
||||
return true;
|
||||
}
|
||||
window.openElectronAuxiliaryWindow = openElectronAuxiliaryWindow;
|
||||
|
||||
// Electron links always point to the public web application rather than a
|
||||
// machine-local file:// URL, so they can be sent directly to another user.
|
||||
var TRIMSP_CANONICAL_SHARE_URL = 'http://musruser.psi.ch/TRIMSP-NL/';
|
||||
|
||||
function encodeElectronSharedConfiguration(config) {
|
||||
let json = JSON.stringify(config);
|
||||
try {
|
||||
let zipped = require('zlib').gzipSync(require('buffer').Buffer.from(json, 'utf8'));
|
||||
return {
|
||||
prefix: '#c=',
|
||||
payload: zipped.toString('base64')
|
||||
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
||||
};
|
||||
} catch (error) {
|
||||
return { prefix: '#cfg=', payload: encodeURIComponent(json) };
|
||||
}
|
||||
}
|
||||
|
||||
async function copyWebConfigurationLink() {
|
||||
let encoded = encodeElectronSharedConfiguration(buildSharedConfiguration());
|
||||
let url = TRIMSP_CANONICAL_SHARE_URL + encoded.prefix + encoded.payload;
|
||||
|
||||
try {
|
||||
await ipcRenderer.invoke('copyConfigurationLink', url);
|
||||
showTransientWarning('Configuration link copied.', document.getElementById('shareConfigButton'), 3500);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (typeof window.prompt === 'function') {
|
||||
window.prompt('Copy configuration link:', url);
|
||||
return true;
|
||||
}
|
||||
showTransientWarning('Could not copy configuration link.', document.getElementById('shareConfigButton'), 3500);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getFiles(dir, filelist){
|
||||
fileList = [];
|
||||
var files = fs.readdirSync(dir);
|
||||
|
||||
@@ -3128,8 +3128,22 @@ function showTransientWarning(message, targetElement, duration = 4000) {
|
||||
warning.textContent = message;
|
||||
if (targetElement) {
|
||||
const rect = targetElement.getBoundingClientRect();
|
||||
warning.style.left = `${Math.max(12, rect.left + window.scrollX)}px`;
|
||||
warning.style.top = `${rect.bottom + window.scrollY + 8}px`;
|
||||
const viewportMargin = 12;
|
||||
const targetGap = 8;
|
||||
const maximumLeft = Math.max(viewportMargin,
|
||||
window.innerWidth - warning.offsetWidth - viewportMargin);
|
||||
const left = Math.min(Math.max(viewportMargin, rect.left), maximumLeft);
|
||||
let top = rect.bottom + targetGap;
|
||||
|
||||
// Keep notifications inside the viewport, including for controls near its
|
||||
// right and bottom edges. Fixed-position elements use viewport coordinates.
|
||||
if (top + warning.offsetHeight > window.innerHeight - viewportMargin) {
|
||||
top = rect.top - warning.offsetHeight - targetGap;
|
||||
}
|
||||
top = Math.max(viewportMargin,
|
||||
Math.min(top, window.innerHeight - warning.offsetHeight - viewportMargin));
|
||||
warning.style.left = `${left}px`;
|
||||
warning.style.top = `${top}px`;
|
||||
}
|
||||
warning.classList.add('visible');
|
||||
|
||||
@@ -3283,6 +3297,7 @@ function rho_fun()
|
||||
}
|
||||
|
||||
function openTab(event, tabName) {
|
||||
window.trimspActiveTab = tabName;
|
||||
// Declare all variables
|
||||
var i, tabcontent, tablinks;
|
||||
// Get all elements with class="tabcontent" and hide them
|
||||
@@ -3300,6 +3315,9 @@ function openTab(event, tabName) {
|
||||
// Show the current tab, and add an "active" class to the button that opened the tab
|
||||
document.getElementById(tabName).style.display = "block";
|
||||
if (event) event.currentTarget.className += " active";
|
||||
if (tabName === "Plots") {
|
||||
requestAnimationFrame(() => resizePl());
|
||||
}
|
||||
}
|
||||
|
||||
function adjust_table()
|
||||
@@ -3351,11 +3369,12 @@ function adjust_table()
|
||||
|
||||
function adjust_scans()
|
||||
{
|
||||
// Deal with scans checkbox and type
|
||||
var scanSeq = document.getElementById("scanSeq").checked;
|
||||
var sender = document.getElementById("scanSeq");
|
||||
var scanType = document.getElementById("scanType").value;
|
||||
var scanThreads = document.getElementById("scanThreads");
|
||||
var scanControls = document.getElementById("ScansControls");
|
||||
var scanRange = document.getElementById("ScansTable");
|
||||
var scanList = document.getElementById("ScansLine");
|
||||
|
||||
// Default to half the available local worker slots, rounded down, while
|
||||
// keeping at least one process. Web mode keeps the default at 1 because
|
||||
@@ -3366,6 +3385,10 @@ function adjust_scans()
|
||||
availableThreads = Math.max(1, getParallelJobCount());
|
||||
}
|
||||
scanThreads.max = availableThreads;
|
||||
scanThreads.disabled = amIWeb();
|
||||
scanThreads.title = amIWeb()
|
||||
? 'The web backend currently runs scan points sequentially.'
|
||||
: `This computer provides up to ${availableThreads} worker slots.`;
|
||||
|
||||
if (scanThreads.value === "" || Number(scanThreads.value) < 1) {
|
||||
scanThreads.value = Math.max(1, Math.floor(availableThreads / 2));
|
||||
@@ -3374,23 +3397,85 @@ function adjust_scans()
|
||||
}
|
||||
}
|
||||
|
||||
// consider only if scans checkbox is checked
|
||||
if (scanSeq) {
|
||||
ShowHide(1,'ScansControls');
|
||||
// Which type of scan
|
||||
if (scanType == "scanLoop") {
|
||||
ShowHide(1,'ScansTable');
|
||||
ShowHide(0,'ScansLine');
|
||||
scanControls.hidden = !scanSeq;
|
||||
if (!scanSeq) {
|
||||
scanRange.hidden = true;
|
||||
scanList.hidden = true;
|
||||
} else if (scanType === "scanLoop") {
|
||||
scanRange.hidden = false;
|
||||
scanList.hidden = true;
|
||||
} else {
|
||||
scanRange.hidden = true;
|
||||
scanList.hidden = false;
|
||||
}
|
||||
|
||||
updateScanSummary();
|
||||
}
|
||||
|
||||
function updateScanSummary()
|
||||
{
|
||||
var summary = document.getElementById("scanSummary");
|
||||
if (!summary) { return; }
|
||||
|
||||
if (!document.getElementById("scanSeq").checked) {
|
||||
summary.dataset.level = "idle";
|
||||
summary.textContent = "Single simulation using the Layers settings.";
|
||||
return;
|
||||
}
|
||||
|
||||
var scanType = document.getElementById("scanType").value;
|
||||
var parameterSelect = document.getElementById("comboScan");
|
||||
var parameter = parameterSelect.options[parameterSelect.selectedIndex].text;
|
||||
var workers = Number(document.getElementById("scanThreads").value) || 1;
|
||||
var pointCount = 0;
|
||||
var detail = "";
|
||||
var warning = "";
|
||||
|
||||
if (scanType === "scanLoop") {
|
||||
var from = Number(document.getElementById("scanFrom").value);
|
||||
var step = Number(document.getElementById("scanStep").value);
|
||||
var to = Number(document.getElementById("scanTo").value);
|
||||
if (!Number.isFinite(from) || !Number.isFinite(step) || !Number.isFinite(to)) {
|
||||
warning = "Enter numeric From, Step, and To values.";
|
||||
} else if (step <= 0) {
|
||||
warning = "Step must be greater than zero.";
|
||||
} else if (to < from) {
|
||||
warning = "To must be greater than or equal to From.";
|
||||
} else {
|
||||
ShowHide(1,'ScansLine');
|
||||
ShowHide(0,'ScansTable');
|
||||
pointCount = Math.floor((to - from) / step) + 1;
|
||||
detail = `${from} to ${to}, step ${step}`;
|
||||
}
|
||||
} else {
|
||||
// Hide both
|
||||
ShowHide(0,'ScansControls');
|
||||
ShowHide(0,'ScansTable');
|
||||
ShowHide(0,'ScansLine');
|
||||
var values = document.getElementById("scanList").value
|
||||
.split(",")
|
||||
.map(value => value.trim())
|
||||
.filter(value => value !== "");
|
||||
var depthValues = document.getElementById("scanListdz").value
|
||||
.split(",")
|
||||
.map(value => value.trim())
|
||||
.filter(value => value !== "");
|
||||
if (values.length === 0) {
|
||||
warning = "Enter at least one scan value.";
|
||||
} else if (values.some(value => !Number.isFinite(Number(value)))) {
|
||||
warning = "Every scan value must be numeric.";
|
||||
} else if (depthValues.length > 0 && depthValues.length !== values.length) {
|
||||
warning = "Depth increments must match the number of scan values.";
|
||||
} else {
|
||||
pointCount = values.length;
|
||||
detail = values.join(", ");
|
||||
}
|
||||
}
|
||||
|
||||
if (warning) {
|
||||
summary.dataset.level = "warning";
|
||||
summary.textContent = warning;
|
||||
return;
|
||||
}
|
||||
|
||||
summary.dataset.level = "ready";
|
||||
var pointLabel = pointCount === 1 ? "point" : "points";
|
||||
var workerLabel = workers === 1 ? "worker" : "workers";
|
||||
summary.textContent = `${parameter} | ${pointCount} ${pointLabel} | ${workers} ${workerLabel} | ${detail}`;
|
||||
}
|
||||
|
||||
function ProjSmartDefaults()
|
||||
@@ -3419,17 +3504,6 @@ function ProjNumberLimit()
|
||||
}
|
||||
|
||||
|
||||
function ShowHide(onoff,item) {
|
||||
// this function can switch item to visible/hidden onoff=1/0
|
||||
if(onoff){
|
||||
// document.getElementById(item).style.display = 'block';
|
||||
document.getElementById(item).style.visibility = "visible";
|
||||
} else {
|
||||
// document.getElementById(item).style.display = 'none';
|
||||
document.getElementById(item).style.visibility = "hidden";
|
||||
}
|
||||
}
|
||||
|
||||
function prep_cfg(toggle) {
|
||||
// This function collects various values and returns
|
||||
// toggle=1 : returns object with all values
|
||||
@@ -3752,10 +3826,25 @@ async function startSequence() {
|
||||
// fields synchronized with the randomized server-side run directory.
|
||||
document.getElementById("fileNamePrefix").value = All["fileNamePrefix"];
|
||||
document.getElementById("workPath").value = All["workPath"];
|
||||
// A new web run replaces the previous archive link and diagnostics.
|
||||
let outputLink = document.getElementById("statusOutputLink");
|
||||
if (outputLink) {
|
||||
outputLink.hidden = true;
|
||||
outputLink.removeAttribute("href");
|
||||
}
|
||||
if (typeof webRunDiagnostics !== "undefined") {
|
||||
webRunDiagnostics.length = 0;
|
||||
}
|
||||
let runLog = document.getElementById("webRunLog");
|
||||
if (runLog) {
|
||||
runLog.textContent = "No web requests have been made yet.";
|
||||
}
|
||||
updateStatusBar({ backend: 'Web backend' });
|
||||
}
|
||||
updateStatusBar({ workPath: All["workPath"], run: 'Starting run...' });
|
||||
writeAsciiFile(All["workPath"] + "/TrimSP.cfg", prep_cfg(0));
|
||||
if (!writeAsciiFile(All["workPath"] + "/TrimSP.cfg", prep_cfg(0))) {
|
||||
return(0);
|
||||
}
|
||||
|
||||
let Progress =0;
|
||||
document.getElementById("pBar").style.width = Progress + "%";
|
||||
@@ -3878,7 +3967,9 @@ async function startSequence() {
|
||||
|
||||
if (webOrApp || jobs.length <= 1 || typeof executeFileAsync !== 'function') {
|
||||
for (const job of jobs) {
|
||||
writeAsciiFile(All["workPath"]+"/"+job.filename+".inp", job.inputFileContent);
|
||||
if (!writeAsciiFile(All["workPath"]+"/"+job.filename+".inp", job.inputFileContent)) {
|
||||
return(0);
|
||||
}
|
||||
if (!webOrApp) {
|
||||
cmd = "cd " + All["workPath"];
|
||||
cmd += ";" + trimBin + ' ' + job.filename + ' ' + rngType;
|
||||
@@ -3942,7 +4033,9 @@ async function startSequence() {
|
||||
let combinedSeqLines = buildCombinedSequenceLines(results);
|
||||
let seq_file = All["workPath"]+"/"+All["fileNamePrefix"]+"_Seq_Results.dat";
|
||||
if (combinedSeqLines.length > 0) {
|
||||
writeAsciiFile(seq_file, combinedSeqLines.join('\n') + '\n');
|
||||
if (!writeAsciiFile(seq_file, combinedSeqLines.join('\n') + '\n')) {
|
||||
return(0);
|
||||
}
|
||||
}
|
||||
Progress=100;
|
||||
document.getElementById("pBar").style.width = Progress + "%";
|
||||
@@ -4183,6 +4276,169 @@ function amIWeb() {
|
||||
}
|
||||
}
|
||||
|
||||
// Share links contain reproducible simulation inputs only. Machine-specific
|
||||
// output paths, prefixes, worker counts, logs, and generated results stay local.
|
||||
var TRIMSP_SHARE_CONFIG_VERSION = 1;
|
||||
var TRIMSP_SHARED_CONTROL_IDS = [
|
||||
'ProjType', 'numberProj', 'z0', 'dz', 'valEnergy', 'sigEnergy',
|
||||
'valAngle', 'sigAngle', 'rngType', 'ranSeed',
|
||||
'scanSeq', 'comboScan', 'scanType', 'scanFrom', 'scanStep', 'scanTo',
|
||||
'scanList', 'scanListdz',
|
||||
'parEF', 'parESB', 'parSHEATH', 'parERC', 'parRD', 'parCA',
|
||||
'parKK0', 'parKK0R', 'parKDEE1', 'parKDEE2', 'parIPOT', 'parIPOTR',
|
||||
'flagSPC', 'parIRL'
|
||||
];
|
||||
|
||||
function readSharedControlValue(element) {
|
||||
if (!element) { return null; }
|
||||
if (element.type === 'checkbox') { return Boolean(element.checked); }
|
||||
return element.value !== undefined ? String(element.value) : '';
|
||||
}
|
||||
|
||||
function buildSharedConfiguration() {
|
||||
let layers = [];
|
||||
let layerCount = Math.max(1, Math.min(100, Number(document.getElementById('numLayer').value) || 1));
|
||||
for (let index = 1; index <= layerCount; index++) {
|
||||
layers.push({
|
||||
composition: String(document.getElementById(`L${index}Comp`).value || ''),
|
||||
density: String(document.getElementById(`L${index}rho`).value || ''),
|
||||
thickness: String(document.getElementById(`L${index}d`).value || '')
|
||||
});
|
||||
}
|
||||
|
||||
let controls = {};
|
||||
for (let id of TRIMSP_SHARED_CONTROL_IDS) {
|
||||
controls[id] = readSharedControlValue(document.getElementById(id));
|
||||
}
|
||||
return { v: TRIMSP_SHARE_CONFIG_VERSION, layers: layers, controls: controls };
|
||||
}
|
||||
|
||||
function validateSharedConfiguration(config) {
|
||||
if (!config || config.v !== TRIMSP_SHARE_CONFIG_VERSION
|
||||
|| !Array.isArray(config.layers) || config.layers.length < 1
|
||||
|| config.layers.length > 100 || typeof config.controls !== 'object'
|
||||
|| config.controls === null) {
|
||||
throw new Error('unsupported configuration format');
|
||||
}
|
||||
|
||||
for (let layer of config.layers) {
|
||||
if (!layer || typeof layer !== 'object') {
|
||||
throw new Error('invalid layer configuration');
|
||||
}
|
||||
for (let key of ['composition', 'density', 'thickness']) {
|
||||
if (typeof layer[key] !== 'string' || layer[key].length > 200) {
|
||||
throw new Error('invalid layer value');
|
||||
}
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
function restoreSharedConfiguration(config) {
|
||||
config = validateSharedConfiguration(config);
|
||||
let layerCount = document.getElementById('numLayer');
|
||||
layerCount.value = String(config.layers.length);
|
||||
adjust_table();
|
||||
|
||||
for (let index = 1; index <= config.layers.length; index++) {
|
||||
let layer = config.layers[index - 1];
|
||||
document.getElementById(`L${index}Comp`).value = layer.composition;
|
||||
document.getElementById(`L${index}rho`).value = layer.density;
|
||||
document.getElementById(`L${index}d`).value = layer.thickness;
|
||||
markDensityEstimated(`L${index}rho`, 0);
|
||||
}
|
||||
|
||||
for (let id of TRIMSP_SHARED_CONTROL_IDS) {
|
||||
if (!Object.prototype.hasOwnProperty.call(config.controls, id)) { continue; }
|
||||
let element = document.getElementById(id);
|
||||
if (!element) { continue; }
|
||||
if (element.type === 'checkbox') {
|
||||
element.checked = Boolean(config.controls[id]);
|
||||
} else if (typeof config.controls[id] === 'string'
|
||||
|| typeof config.controls[id] === 'number') {
|
||||
element.value = String(config.controls[id]);
|
||||
}
|
||||
}
|
||||
adjust_scans();
|
||||
return true;
|
||||
}
|
||||
|
||||
// The active tab selects the highlighted section without splitting help into
|
||||
// separate documents or interrupting the user's current simulation setup.
|
||||
function quickStartSectionForTab(tabName) {
|
||||
let sections = {
|
||||
Layers: 'helpLayers',
|
||||
Scans: 'helpScans',
|
||||
Plots: 'helpResults',
|
||||
Other: 'helpAdvanced'
|
||||
};
|
||||
return sections[tabName] || 'helpLayers';
|
||||
}
|
||||
|
||||
function makeQuickStartDialogDraggable(dialog) {
|
||||
if (!dialog || dialog.dataset.dragReady === '1') { return; }
|
||||
let handle = dialog.querySelector('.quick-start-drag-handle');
|
||||
if (!handle) { return; }
|
||||
dialog.dataset.dragReady = '1';
|
||||
let dragPosition = null;
|
||||
|
||||
handle.addEventListener('pointerdown', function(event) {
|
||||
if (event.target.closest('button')) { return; }
|
||||
let bounds = dialog.getBoundingClientRect();
|
||||
dragPosition = {
|
||||
pointerX: event.clientX,
|
||||
pointerY: event.clientY,
|
||||
windowX: bounds.left,
|
||||
windowY: bounds.top
|
||||
};
|
||||
handle.setPointerCapture(event.pointerId);
|
||||
event.preventDefault();
|
||||
});
|
||||
handle.addEventListener('pointermove', function(event) {
|
||||
if (!dragPosition) { return; }
|
||||
let maximumX = Math.max(0, window.innerWidth - dialog.offsetWidth);
|
||||
let maximumY = Math.max(0, window.innerHeight - handle.offsetHeight);
|
||||
let nextX = dragPosition.windowX + event.clientX - dragPosition.pointerX;
|
||||
let nextY = dragPosition.windowY + event.clientY - dragPosition.pointerY;
|
||||
dialog.style.left = Math.min(maximumX, Math.max(0, nextX)) + 'px';
|
||||
dialog.style.top = Math.min(maximumY, Math.max(0, nextY)) + 'px';
|
||||
});
|
||||
handle.addEventListener('pointerup', function(event) {
|
||||
dragPosition = null;
|
||||
if (handle.hasPointerCapture(event.pointerId)) {
|
||||
handle.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showQuickStartHelp() {
|
||||
let sectionId = quickStartSectionForTab(window.trimspActiveTab || 'Layers');
|
||||
if (typeof window.openElectronAuxiliaryWindow === 'function'
|
||||
&& window.openElectronAuxiliaryWindow('quickStart', { sectionId: sectionId })) {
|
||||
return;
|
||||
}
|
||||
|
||||
let dialog = document.getElementById('quickStartDialog');
|
||||
let frame = document.getElementById('quickStartFrame');
|
||||
if (!dialog || !frame) { return; }
|
||||
let source = 'quick-start.html#' + sectionId;
|
||||
if (frame.getAttribute('src') !== source) { frame.setAttribute('src', source); }
|
||||
makeQuickStartDialogDraggable(dialog);
|
||||
if (!dialog.open) {
|
||||
dialog.show();
|
||||
dialog.style.left = Math.max(12, (window.innerWidth - dialog.offsetWidth) / 2) + 'px';
|
||||
dialog.style.top = '48px';
|
||||
}
|
||||
dialog.focus();
|
||||
}
|
||||
|
||||
function closeQuickStartHelp() {
|
||||
let dialog = document.getElementById('quickStartDialog');
|
||||
if (!dialog) { return; }
|
||||
if (typeof dialog.close === 'function' && dialog.open) { dialog.close(); }
|
||||
else { dialog.removeAttribute('open'); }
|
||||
}
|
||||
|
||||
async function getDensity(formula) {
|
||||
function normalizeCodFormula(codFormula) {
|
||||
return codFormula.replace(/-/g, ' ').replace(/\s+/g, '').trim();
|
||||
|
||||
@@ -1,135 +1,232 @@
|
||||
// This file contains function that are web app specific
|
||||
|
||||
let webRunDiagnostics = [];
|
||||
|
||||
function bytesToBase64URL(bytes) {
|
||||
let binary = '';
|
||||
const chunkSize = 0x8000;
|
||||
for (let index = 0; index < bytes.length; index += chunkSize) {
|
||||
binary += String.fromCharCode.apply(null, bytes.subarray(index, index + chunkSize));
|
||||
}
|
||||
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||
}
|
||||
|
||||
function base64URLToBytes(text) {
|
||||
text = String(text || '').replace(/-/g, '+').replace(/_/g, '/');
|
||||
while (text.length % 4) { text += '='; }
|
||||
let binary = atob(text);
|
||||
let bytes = new Uint8Array(binary.length);
|
||||
for (let index = 0; index < binary.length; index++) {
|
||||
bytes[index] = binary.charCodeAt(index);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
async function transformShareBytes(bytes, stream) {
|
||||
let response = new Response(new Blob([bytes]).stream().pipeThrough(stream));
|
||||
return new Uint8Array(await response.arrayBuffer());
|
||||
}
|
||||
|
||||
async function encodeWebSharedConfiguration(config) {
|
||||
let json = JSON.stringify(config);
|
||||
if (typeof CompressionStream !== 'function') {
|
||||
return { prefix: '#cfg=', payload: encodeURIComponent(json) };
|
||||
}
|
||||
let bytes = new TextEncoder().encode(json);
|
||||
let zipped = await transformShareBytes(bytes, new CompressionStream('gzip'));
|
||||
return { prefix: '#c=', payload: bytesToBase64URL(zipped) };
|
||||
}
|
||||
|
||||
async function decodeWebSharedConfiguration(hash) {
|
||||
hash = String(hash || '');
|
||||
let compressed = hash.startsWith('#c=');
|
||||
let uncompressed = hash.startsWith('#cfg=');
|
||||
if (!compressed && !uncompressed) { return null; }
|
||||
if (hash.length > 500000) { throw new Error('configuration link is too large'); }
|
||||
|
||||
let json;
|
||||
if (compressed) {
|
||||
if (typeof DecompressionStream !== 'function') {
|
||||
throw new Error('this browser cannot decompress shared configurations');
|
||||
}
|
||||
let zipped = base64URLToBytes(hash.substring(3));
|
||||
if (zipped.length > 100000) { throw new Error('configuration link is too large'); }
|
||||
let bytes = await transformShareBytes(zipped, new DecompressionStream('gzip'));
|
||||
if (bytes.length > 500000) { throw new Error('configuration is too large'); }
|
||||
json = new TextDecoder().decode(bytes);
|
||||
} else {
|
||||
json = decodeURIComponent(hash.substring(5));
|
||||
}
|
||||
return validateSharedConfiguration(JSON.parse(json));
|
||||
}
|
||||
|
||||
function webShareBaseURL() {
|
||||
let location = window.location || {};
|
||||
let origin = location.origin && location.origin !== 'null' ? location.origin : '';
|
||||
return origin + (location.pathname || '/');
|
||||
}
|
||||
|
||||
async function copyWebConfigurationLink() {
|
||||
let encoded;
|
||||
try {
|
||||
encoded = await encodeWebSharedConfiguration(buildSharedConfiguration());
|
||||
} catch (error) {
|
||||
showTransientWarning('Could not create configuration link.', document.getElementById('shareConfigButton'), 3500);
|
||||
return false;
|
||||
}
|
||||
let url = webShareBaseURL() + encoded.prefix + encoded.payload;
|
||||
|
||||
try {
|
||||
if (!navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') {
|
||||
throw new Error('clipboard API unavailable');
|
||||
}
|
||||
await navigator.clipboard.writeText(url);
|
||||
showTransientWarning('Configuration link copied.', document.getElementById('shareConfigButton'), 3500);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (typeof window.prompt === 'function') {
|
||||
window.prompt('Copy configuration link:', url);
|
||||
return true;
|
||||
}
|
||||
showTransientWarning('Could not copy configuration link.', document.getElementById('shareConfigButton'), 3500);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function initializeWebSharedConfiguration() {
|
||||
let hash = window.location && typeof window.location.hash === 'string'
|
||||
? window.location.hash : '';
|
||||
if (!hash.startsWith('#c=') && !hash.startsWith('#cfg=')) { return false; }
|
||||
|
||||
try {
|
||||
let config = await decodeWebSharedConfiguration(hash);
|
||||
restoreSharedConfiguration(config);
|
||||
window.history.replaceState({}, '', (window.location.pathname || '/') + (window.location.search || ''));
|
||||
showTransientWarning('Shared configuration loaded.', document.getElementById('shareConfigButton'), 3500);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Could not restore shared configuration:', error);
|
||||
showTransientWarning('Could not load shared configuration.', document.getElementById('shareConfigButton'), 4500);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('load', function() {
|
||||
if (typeof amIWeb === 'function' && amIWeb()) {
|
||||
initializeWebSharedConfiguration();
|
||||
}
|
||||
});
|
||||
|
||||
function openWebRunDetails() {
|
||||
let dialog = document.getElementById("runDetailsDialog");
|
||||
if (dialog && !dialog.open) {
|
||||
dialog.showModal();
|
||||
}
|
||||
}
|
||||
|
||||
function recordWebDiagnostic(filename, request, response) {
|
||||
let timestamp = new Date().toLocaleTimeString();
|
||||
let entry = `[${timestamp}] ${filename}\nHTTP ${request.status} ${request.statusText || ""}`;
|
||||
|
||||
if (response) {
|
||||
entry += `\nPhase: ${response.phase || "unknown"}`;
|
||||
entry += `\nStatus: ${response.ok ? "successful" : "failed"}`;
|
||||
if (response.exitCode !== null && response.exitCode !== undefined) {
|
||||
entry += `\nBinary exit code: ${response.exitCode}`;
|
||||
}
|
||||
if (response.message) {
|
||||
entry += `\nCGI: ${response.message}`;
|
||||
}
|
||||
if (response.log) {
|
||||
entry += `\n\n${response.log.trim()}`;
|
||||
}
|
||||
} else if (request.responseText) {
|
||||
entry += `\n\n${request.responseText.trim()}`;
|
||||
}
|
||||
|
||||
webRunDiagnostics.push(entry);
|
||||
let log = document.getElementById("webRunLog");
|
||||
if (log) {
|
||||
log.textContent = webRunDiagnostics.join("\n\n----------------------------------------\n\n");
|
||||
log.scrollTop = log.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
function writeAsciiFile(filename,content) {
|
||||
// Write string content into ascii file filename
|
||||
// Go via CGI script
|
||||
//console.log(filename, content);
|
||||
// Prepare CGI args use POST for long files
|
||||
let cgiargs = "fn="+filename; // POST
|
||||
let cgiargs = new URLSearchParams();
|
||||
cgiargs.set("fn", filename);
|
||||
// Forward the GUI-selected RNG backend to the CGI wrapper, which passes it
|
||||
// to the TRIM.SP-NL engine as the optional second command-line argument.
|
||||
let rngType = document.getElementById("rngType");
|
||||
if (rngType) {
|
||||
cgiargs += "&rngType=" + rngType.value;
|
||||
cgiargs.set("rngType", rngType.value);
|
||||
}
|
||||
let lines = content.split(/\n/);
|
||||
let prefix = filename.split(/\//);
|
||||
for (let i=0; i<lines.length; i++) {
|
||||
cgiargs += "&line" + i + "=" + lines[i].replace(/\s\s+/g, ' ');
|
||||
cgiargs.set("line" + i, lines[i].replace(/\s\s+/g, ' '));
|
||||
}
|
||||
|
||||
var xhttp;
|
||||
xhttp = new XMLHttpRequest();
|
||||
xhttp.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
console.log("response=",xhttp.responseText);
|
||||
} else if (this.readyState == 4) {
|
||||
console.log(xhttp.status, xhttp.statusText);
|
||||
}
|
||||
}
|
||||
let request = "/cgi-bin/singleTrimSP.cgi"; //POST
|
||||
xhttp.open("POST", request, false); //POST
|
||||
|
||||
//Send the proper header information along with the request
|
||||
xhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); //POST
|
||||
xhttp.send(cgiargs); //POST
|
||||
// Now you can add a link to the files
|
||||
let d = document.getElementById("linkDiv");
|
||||
if (!d) {
|
||||
d = document.createElement("div");
|
||||
d.id = "linkDiv";
|
||||
document.body.appendChild(d);
|
||||
}
|
||||
let htmlLink = "Download files: <a href='";
|
||||
htmlLink += "/tmp/" + prefix[2] + ".tgz'>";
|
||||
htmlLink += prefix[2] + ".tgz</a>";
|
||||
d.innerHTML = htmlLink;
|
||||
|
||||
return(1);
|
||||
}
|
||||
xhttp.send(cgiargs.toString()); //POST
|
||||
|
||||
function writeAsciiFile2(filename,content) {
|
||||
// Write string content into ascii file filename
|
||||
// Go via CGI script
|
||||
//console.log(filename, content);
|
||||
// Prepare CGI args use GET for short files
|
||||
let cgiargs = "?fn="+filename; // GET
|
||||
// Keep GET and POST requests equivalent for callers that still use this
|
||||
// short-file helper.
|
||||
let rngType = document.getElementById("rngType");
|
||||
if (rngType) {
|
||||
cgiargs += "&rngType=" + rngType.value;
|
||||
let response = null;
|
||||
try {
|
||||
response = JSON.parse(xhttp.responseText);
|
||||
} catch (error) {
|
||||
console.error("Invalid CGI response", error, xhttp.responseText);
|
||||
}
|
||||
let lines = content.split(/\n/);
|
||||
let prefix = filename.split(/\//);
|
||||
for (let i=0; i<lines.length; i++) {
|
||||
cgiargs += "&line" + i + "=" + lines[i].replace(/\s\s+/g, ' ');
|
||||
recordWebDiagnostic(filename, xhttp, response);
|
||||
|
||||
if (xhttp.status < 200 || xhttp.status >= 300 || !response || !response.ok) {
|
||||
updateStatusBar({ run: `Web backend failed for ${prefix[prefix.length - 1]}` });
|
||||
return(0);
|
||||
}
|
||||
|
||||
var xhttp;
|
||||
xhttp = new XMLHttpRequest();
|
||||
xhttp.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
console.log("response=",xhttp.responseText);
|
||||
} else if (this.readyState == 4) {
|
||||
console.log(xhttp.status, xhttp.statusText);
|
||||
}
|
||||
if (response.archiveUrl) {
|
||||
let outputLink = document.getElementById("statusOutputLink");
|
||||
outputLink.href = response.archiveUrl;
|
||||
outputLink.textContent = "Outputs";
|
||||
outputLink.title = `Download ${prefix[2]}.tgz`;
|
||||
outputLink.hidden = false;
|
||||
}
|
||||
let request = "/cgi-bin/singleTrimSP.cgi"+cgiargs; // GET
|
||||
xhttp.open("GET", request, false); //GET
|
||||
|
||||
//Send the proper header information along with the request
|
||||
xhttp.send(); // GET
|
||||
// Now you can add a link to the files
|
||||
let d = document.getElementById("linkDiv");
|
||||
if (!d) {
|
||||
d = document.createElement("div");
|
||||
d.id = "linkDiv";
|
||||
document.body.appendChild(d);
|
||||
}
|
||||
let htmlLink = "Download files: <a href='";
|
||||
htmlLink += "/tmp/" + prefix[2] + ".tgz'>";
|
||||
htmlLink += prefix[2] + ".tgz</a>";
|
||||
d.innerHTML = htmlLink;
|
||||
|
||||
return(1);
|
||||
}
|
||||
|
||||
function readAsciiFile(filename) {
|
||||
// Read ascii file filename and return content in a string
|
||||
var xhttp;
|
||||
let file = filename;
|
||||
// Read server-side simulation output through the CGI. Files remain in
|
||||
// filesystem /tmp and do not need to be exposed directly by Apache.
|
||||
if (filename == "" || filename == undefined || filename == null) {
|
||||
return 0;
|
||||
} else {
|
||||
// Make an HTTP request
|
||||
xhttp = new XMLHttpRequest();
|
||||
xhttp.onreadystatechange = function() {
|
||||
if (this.readyState == 4) {
|
||||
if (this.status == 404) {
|
||||
console.log("File "+file+" not found.");
|
||||
return(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Force refresh, do not use cache
|
||||
filename += "?" + Date();
|
||||
//filename = "http://musruser.psi.ch" + filename;
|
||||
xhttp.open("GET", filename, false);
|
||||
xhttp.overrideMimeType("text/plain");
|
||||
xhttp.send();
|
||||
return(xhttp.responseText);
|
||||
}
|
||||
}
|
||||
|
||||
function fileExists(filename) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('HEAD', filename, false);
|
||||
xhr.send();
|
||||
|
||||
if (xhr.status == "404") {
|
||||
return 0;
|
||||
} else {
|
||||
return 1;
|
||||
|
||||
let request = "/cgi-bin/singleTrimSP.cgi?action=read&fn="
|
||||
+ encodeURIComponent(filename) + "&_=" + Date.now();
|
||||
let xhttp = new XMLHttpRequest();
|
||||
xhttp.open("GET", request, false);
|
||||
xhttp.overrideMimeType("text/plain");
|
||||
xhttp.send();
|
||||
|
||||
if (xhttp.status < 200 || xhttp.status >= 300) {
|
||||
recordWebDiagnostic(filename, xhttp, {
|
||||
ok: false,
|
||||
phase: "read",
|
||||
message: `Could not read simulation output (HTTP ${xhttp.status}).`,
|
||||
});
|
||||
updateStatusBar({ run: `Could not read ${filename.split("/").pop()}` });
|
||||
throw new Error(`Could not read ${filename}: HTTP ${xhttp.status} ${xhttp.statusText}`);
|
||||
}
|
||||
|
||||
return(xhttp.responseText);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 509 B After Width: | Height: | Size: 761 B |
|
Before Width: | Height: | Size: 772 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 364 KiB |
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" role="img" aria-labelledby="title description">
|
||||
<title id="title">TRIMSP-NL Workbench icon</title>
|
||||
<desc id="description">An orange particle trajectory stopping as a spin-polarized muon inside a layered teal target.</desc>
|
||||
<defs>
|
||||
<linearGradient id="background" x1="140" y1="80" x2="880" y2="960" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#214b56"/>
|
||||
<stop offset="1" stop-color="#102f38"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="trajectory" x1="110" y1="130" x2="456" y2="686" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#e58454"/>
|
||||
<stop offset="1" stop-color="#bd5f37"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="stopPoint" cx="42%" cy="35%" r="65%">
|
||||
<stop offset="0" stop-color="#fff9df"/>
|
||||
<stop offset="0.45" stop-color="#f4d48a"/>
|
||||
<stop offset="1" stop-color="#d27a43"/>
|
||||
</radialGradient>
|
||||
<clipPath id="tile">
|
||||
<rect x="48" y="48" width="928" height="928" rx="208"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<g>
|
||||
<rect x="48" y="48" width="928" height="928" rx="208" fill="url(#background)"/>
|
||||
<g clip-path="url(#tile)">
|
||||
<!-- Three target layers remain distinct even at taskbar icon sizes. -->
|
||||
<path d="M48 444 C276 405 503 483 976 420 V622 H48 Z" fill="#f2ead8"/>
|
||||
<path d="M48 596 C332 553 615 660 976 578 V802 H48 Z" fill="#75aeb4"/>
|
||||
<path d="M48 774 C322 730 647 842 976 747 V976 H48 Z" fill="#376a70"/>
|
||||
<path d="M48 444 C276 405 503 483 976 420" fill="none" stroke="#fff9ec" stroke-opacity="0.72" stroke-width="12"/>
|
||||
<path d="M48 596 C332 553 615 660 976 578" fill="none" stroke="#315f68" stroke-opacity="0.42" stroke-width="10"/>
|
||||
<path d="M48 774 C322 730 647 842 976 747" fill="none" stroke="#173943" stroke-opacity="0.55" stroke-width="10"/>
|
||||
|
||||
<!-- The trajectory preserves the physical idea of implantation and stopping. -->
|
||||
<path d="M190 160 C414 181 725 232 764 380 C799 520 589 546 463 667"
|
||||
fill="none" stroke="#7d321c" stroke-opacity="0.28" stroke-width="70"
|
||||
stroke-linecap="round" transform="translate(0 14)"/>
|
||||
<path d="M190 160 C414 181 725 232 764 380 C799 520 589 546 463 667"
|
||||
fill="none" stroke="url(#trajectory)" stroke-width="70" stroke-linecap="round"/>
|
||||
<!-- The spin axis passes behind the implanted muon. -->
|
||||
<path d="M355 762 L545 572" fill="none" stroke="#fff6d6" stroke-width="40" stroke-linecap="round"/>
|
||||
<path d="M355 762 L545 572" fill="none" stroke="#173943" stroke-width="22" stroke-linecap="round"/>
|
||||
<path d="M570 547 L553 600 L517 564 Z" fill="#173943" stroke="#fff6d6" stroke-width="9" stroke-linejoin="round"/>
|
||||
<circle cx="450" cy="681" r="77" fill="#0d2931" fill-opacity="0.24"/>
|
||||
<circle cx="450" cy="667" r="60" fill="url(#stopPoint)" stroke="#fff6d6" stroke-width="10"/>
|
||||
</g>
|
||||
<rect x="54" y="54" width="916" height="916" rx="202" fill="none" stroke="#eaf2ee" stroke-opacity="0.24" stroke-width="12"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -1,4 +1,5 @@
|
||||
const path = require('path');
|
||||
const redhatInstaller = require('electron-installer-redhat');
|
||||
const redhatDependencies = require('electron-installer-redhat/src/dependencies');
|
||||
|
||||
const linuxIconSet = {
|
||||
@@ -13,12 +14,20 @@ const linuxIconSet = {
|
||||
};
|
||||
|
||||
const linuxDesktopTemplate = path.resolve(__dirname, 'packaging/linux/trimsp.desktop.ejs');
|
||||
const linuxRpmSpecTemplate = path.resolve(__dirname, 'packaging/linux/trimsp.spec.ejs');
|
||||
const includeFlatpak = process.env.FORGE_FLATPAK === '1';
|
||||
|
||||
// The upstream RPM maker assumes Fedora-style package names. Mageia provides
|
||||
// GTK 3 under lib64gtk+3_0, so allow either name in the generated Requires.
|
||||
redhatDependencies.dependencyMap.gtk3 = '(gtk3 or lib64gtk+3_0)';
|
||||
|
||||
// RPM 4.20 runs %install from an automatically generated build directory.
|
||||
// Use our absolute-path template instead of the installer's relative usr/* copy.
|
||||
redhatInstaller.Installer.prototype.createSpec = function () {
|
||||
this.options.logger(`Creating package spec at ${this.specPath}`);
|
||||
return this.createTemplatedFile(linuxRpmSpecTemplate, this.specPath);
|
||||
};
|
||||
|
||||
const makers = [
|
||||
{
|
||||
name: '@electron-forge/maker-squirrel',
|
||||
@@ -33,25 +42,29 @@ const makers = [
|
||||
{
|
||||
name: '@electron-forge/maker-deb',
|
||||
config: {
|
||||
name: 'trimsp',
|
||||
productName: 'TRIMSP-NL Workbench',
|
||||
genericName: 'Ion implantation simulator',
|
||||
description: 'Workbench for low-energy muon and ion implantation simulations with TRIM.SP-NL.',
|
||||
categories: ['Science'],
|
||||
icon: linuxIconSet,
|
||||
desktopTemplate: linuxDesktopTemplate,
|
||||
options: {
|
||||
name: 'trimsp',
|
||||
productName: 'TRIMSP-NL Workbench',
|
||||
genericName: 'Ion implantation simulator',
|
||||
description: 'Workbench for low-energy muon and ion implantation simulations with TRIM.SP-NL.',
|
||||
categories: ['Science'],
|
||||
icon: linuxIconSet,
|
||||
desktopTemplate: linuxDesktopTemplate,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '@electron-forge/maker-rpm',
|
||||
config: {
|
||||
name: 'trimsp',
|
||||
productName: 'TRIMSP-NL Workbench',
|
||||
genericName: 'Ion implantation simulator',
|
||||
description: 'Workbench for low-energy muon and ion implantation simulations with TRIM.SP-NL.',
|
||||
categories: ['Science'],
|
||||
icon: linuxIconSet,
|
||||
desktopTemplate: linuxDesktopTemplate,
|
||||
options: {
|
||||
name: 'trimsp',
|
||||
productName: 'TRIMSP-NL Workbench',
|
||||
genericName: 'Ion implantation simulator',
|
||||
description: 'Workbench for low-energy muon and ion implantation simulations with TRIM.SP-NL.',
|
||||
categories: ['Science'],
|
||||
icon: linuxIconSet,
|
||||
desktopTemplate: linuxDesktopTemplate,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -93,9 +106,11 @@ module.exports = {
|
||||
'Readme.md',
|
||||
'ToDo.txt',
|
||||
'TrimSP.cfg',
|
||||
'^/trimspNL$',
|
||||
'fortran/*',
|
||||
'(^|/)packaging($|/)',
|
||||
'(^|/)resources/bin($|/)',
|
||||
'(^|/)tests?($|/)',
|
||||
'(^|/)tests?[^/]*($|/)',
|
||||
'(^|/)__tests__($|/)',
|
||||
'(^|/)(test|spec)($|/)',
|
||||
'\\.flatpak-builder($|/)',
|
||||
|
||||
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 102 KiB |
@@ -1,12 +1,51 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { app, BrowserWindow, Menu, dialog, ipcMain } = require('electron');
|
||||
const { spawnSync } = require('child_process');
|
||||
const { app, BrowserWindow, Menu, clipboard, dialog, ipcMain, nativeImage } = require('electron');
|
||||
|
||||
let currentWorkPath = process.cwd();
|
||||
const startupOptions = parseStartupOptions(process.argv);
|
||||
let quickStartWindow = null;
|
||||
|
||||
// Keep the internal Linux window class stable for existing desktop launchers.
|
||||
app.setName('TrimSP');
|
||||
// Packaged builds retain the class expected by installed desktop launchers.
|
||||
// A distinct development class prevents npm start from inheriting the icon
|
||||
// cached for an older system installation of TrimSP.
|
||||
app.setName(app.isPackaged ? 'TrimSP' : 'TrimSPDev');
|
||||
if (process.platform === 'linux' && !app.isPackaged) {
|
||||
// KDE resolves taskbar icons through matching desktop entries and ignores
|
||||
// Electron's development-window icon. Keep a hidden per-user entry tied
|
||||
// to this checkout so npm start uses the current repository artwork.
|
||||
try {
|
||||
const dataHome = process.env.XDG_DATA_HOME || path.join(app.getPath('home'), '.local', 'share');
|
||||
const applicationsDir = path.join(dataHome, 'applications');
|
||||
const desktopFile = path.join(applicationsDir, 'trimsp-dev.desktop');
|
||||
const executable = process.execPath.replace(/"/g, '\\"');
|
||||
const projectPath = __dirname.replace(/"/g, '\\"');
|
||||
const iconPath = path.join(__dirname, 'icon.png');
|
||||
const desktopContent = [
|
||||
'[Desktop Entry]',
|
||||
'Type=Application',
|
||||
'Name=TRIMSP-NL Workbench (Development)',
|
||||
`Exec="${executable}" "${projectPath}"`,
|
||||
`Icon=${iconPath}`,
|
||||
`X-TrimSP-Icon-MTime=${fs.statSync(iconPath).mtimeMs}`,
|
||||
'StartupWMClass=TrimSPDev',
|
||||
'NoDisplay=true',
|
||||
'Categories=Science;',
|
||||
'',
|
||||
].join('\n');
|
||||
fs.mkdirSync(applicationsDir, {recursive: true});
|
||||
if (!fs.existsSync(desktopFile) || fs.readFileSync(desktopFile, 'utf8') !== desktopContent) {
|
||||
fs.writeFileSync(desktopFile, desktopContent, 'utf8');
|
||||
// KDE caches desktop identities and icons; refresh that user cache only
|
||||
// when this entry changes. Other desktops safely ignore ENOENT here.
|
||||
spawnSync('kbuildsycoca6', ['--noincremental'], {stdio: 'ignore'});
|
||||
}
|
||||
app.setDesktopName('trimsp-dev.desktop');
|
||||
} catch (error) {
|
||||
console.warn('Could not register the development desktop entry:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function parseStartupOptions(argv) {
|
||||
const options = {
|
||||
@@ -96,7 +135,7 @@ function parseStartupOptions(argv) {
|
||||
function showAboutDialog(parentWindow) {
|
||||
const detail = [
|
||||
'Graphical workbench for low-energy muon and ion implantation simulations.',
|
||||
`Version: ${app.getVersion()}`,
|
||||
`Workbench version: ${app.getVersion()}`,
|
||||
'License: GNU General Public License v3 or later (GPL-3.0-or-later).',
|
||||
'Maintained by the Low Energy Muons group at the Paul Scherrer Institute.',
|
||||
'Main contributor: Zaher Salman.',
|
||||
@@ -151,11 +190,43 @@ function getWindowIconPath() {
|
||||
return path.join(__dirname, 'icon.png');
|
||||
}
|
||||
|
||||
function showQuickStartWindow(sectionId = 'helpLayers') {
|
||||
if (quickStartWindow && !quickStartWindow.isDestroyed()) {
|
||||
quickStartWindow.show();
|
||||
quickStartWindow.focus();
|
||||
quickStartWindow.webContents.send('quickStartSection', { sectionId: sectionId });
|
||||
return;
|
||||
}
|
||||
|
||||
quickStartWindow = new BrowserWindow({
|
||||
title: 'TRIMSP-NL Quick Start',
|
||||
width: 720,
|
||||
height: 680,
|
||||
minWidth: 480,
|
||||
minHeight: 360,
|
||||
resizable: true,
|
||||
autoHideMenuBar: true,
|
||||
icon: nativeImage.createFromPath(getWindowIconPath()),
|
||||
webPreferences: {
|
||||
contextIsolation: false,
|
||||
nodeIntegration: true,
|
||||
}
|
||||
});
|
||||
quickStartWindow.webContents.once('did-finish-load', () => {
|
||||
quickStartWindow.webContents.send('quickStartSection', { sectionId: sectionId });
|
||||
});
|
||||
quickStartWindow.loadFile('quick-start.html');
|
||||
quickStartWindow.on('closed', () => {
|
||||
quickStartWindow = null;
|
||||
});
|
||||
}
|
||||
|
||||
function createWindow () {
|
||||
const windowIcon = nativeImage.createFromPath(getWindowIconPath());
|
||||
const win = new BrowserWindow({
|
||||
width: 1000,
|
||||
height: 610,
|
||||
icon: getWindowIconPath(),
|
||||
icon: windowIcon,
|
||||
webPreferences: {
|
||||
contextIsolation: false,
|
||||
nodeIntegration: true,
|
||||
@@ -164,6 +235,13 @@ function createWindow () {
|
||||
}
|
||||
})
|
||||
|
||||
// Pass a native image explicitly for Linux panels that can use the window
|
||||
// icon directly. KDE resolves the development icon through the matching
|
||||
// desktop entry registered above.
|
||||
if (process.platform === 'linux' && !windowIcon.isEmpty()) {
|
||||
win.setIcon(windowIcon);
|
||||
}
|
||||
|
||||
let template = [
|
||||
{
|
||||
label: 'File',
|
||||
@@ -356,6 +434,11 @@ function createWindow () {
|
||||
{
|
||||
role: 'help',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Quick Start',
|
||||
accelerator: 'F1',
|
||||
click () { showQuickStartWindow(); }
|
||||
},
|
||||
{
|
||||
label: 'About TRIMSP-NL Workbench',
|
||||
click () { showAboutDialog(win); }
|
||||
@@ -417,6 +500,20 @@ ipcMain.handle('getStartupOptions', async () => {
|
||||
return startupOptions;
|
||||
});
|
||||
|
||||
ipcMain.on('openQuickStartWindow', (event, options) => {
|
||||
const sectionId = options && options.sectionId ? options.sectionId : 'helpLayers';
|
||||
showQuickStartWindow(sectionId);
|
||||
});
|
||||
|
||||
ipcMain.handle('copyConfigurationLink', async (event, url) => {
|
||||
if (typeof url !== 'string' || url.length > 500000
|
||||
|| !url.startsWith('http://musruser.psi.ch/TRIMSP-NL/')) {
|
||||
throw new Error('Invalid configuration link');
|
||||
}
|
||||
clipboard.writeText(url);
|
||||
return true;
|
||||
});
|
||||
|
||||
//ipcMain.on('browseFolder-send', (event, args) => {
|
||||
// dialog.showOpenDialog(null, args).then(filePaths => {
|
||||
// event.sender.dend('browseFolder', filePaths);
|
||||
|
||||
@@ -94,21 +94,18 @@ function sizePlot(){
|
||||
}
|
||||
|
||||
function resizePl(flag){
|
||||
let newx=this.innerWidth/2-50;
|
||||
let newy=this.innerHeight-185;
|
||||
var plotDiv1=this.document.getElementById("plotRge");
|
||||
var plotDiv2=this.document.getElementById("plotFrac");
|
||||
var update ={
|
||||
width: newx,
|
||||
height: newy
|
||||
};
|
||||
var plotDiv1=document.getElementById("plotRge");
|
||||
var plotDiv2=document.getElementById("plotFrac");
|
||||
const canRelayout = (plotDiv) => plotDiv && plotDiv.data && plotDiv.layout;
|
||||
if (flag == null) {
|
||||
if (canRelayout(plotDiv1)) Plotly.relayout(plotDiv1,update);
|
||||
if (canRelayout(plotDiv2)) Plotly.relayout(plotDiv2,update);
|
||||
} else if (flag == 1) {
|
||||
if (canRelayout(plotDiv1)) Plotly.relayout(plotDiv1,update);
|
||||
} else if ( flag == 2) {
|
||||
if (canRelayout(plotDiv2)) Plotly.relayout(plotDiv2,update);
|
||||
|
||||
let plotDivs = [];
|
||||
if (flag == null || flag == 1) plotDivs.push(plotDiv1);
|
||||
if (flag == null || flag == 2) plotDivs.push(plotDiv2);
|
||||
|
||||
for (const plotDiv of plotDivs) {
|
||||
if (!canRelayout(plotDiv) || !plotDiv.parentElement) continue;
|
||||
let width = Math.max(280, plotDiv.parentElement.clientWidth - 8);
|
||||
let height = Math.max(300, plotDiv.parentElement.clientHeight - 8);
|
||||
Plotly.relayout(plotDiv, {width: width, height: height});
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "TrimSP",
|
||||
"version": "1.4.0",
|
||||
"version": "1.5.0",
|
||||
"description": "TRIMSP-NL Workbench for low-energy muon and ion implantation simulations.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
@@ -29,13 +29,13 @@
|
||||
"author": "Zaher Salman",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "^6.0.0-beta.57",
|
||||
"@electron-forge/maker-deb": "^6.0.0-beta.57",
|
||||
"@electron-forge/maker-flatpak": "^7.11.1",
|
||||
"@electron-forge/maker-rpm": "^6.0.0-beta.57",
|
||||
"@electron-forge/maker-squirrel": "^6.0.0-beta.57",
|
||||
"@electron-forge/maker-zip": "^6.0.0-beta.57",
|
||||
"electron": "40.6.0"
|
||||
"@electron-forge/cli": "7.11.2",
|
||||
"@electron-forge/maker-deb": "7.11.2",
|
||||
"@electron-forge/maker-flatpak": "7.11.2",
|
||||
"@electron-forge/maker-rpm": "7.11.2",
|
||||
"@electron-forge/maker-squirrel": "7.11.2",
|
||||
"@electron-forge/maker-zip": "7.11.2",
|
||||
"electron": "44.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"plotly.js-dist": "^1.58.4"
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
%define _binary_payload w<%= compressionLevel %>.xzdio
|
||||
|
||||
%if "%{_host_cpu}" != "%{_target_cpu}"
|
||||
%global __strip /bin/true
|
||||
%endif
|
||||
|
||||
Name: <%= name %>
|
||||
Version: <%= version %>
|
||||
Release: <%= revision %>%{?dist}
|
||||
<% if (description) { print(`Summary: ${description}\n`) }
|
||||
%>
|
||||
<% if (license) { print(`License: ${license}\n`) }
|
||||
if (homepage) { print(`URL: ${homepage}\n`) }
|
||||
if (license || homepage) { print('\n') }
|
||||
|
||||
%>Requires: <%= requires.join(', ') %>
|
||||
AutoReqProv: no
|
||||
|
||||
<% if (productDescription) {
|
||||
%>%description
|
||||
<% print(productDescription)
|
||||
print('\n\n\n') }
|
||||
|
||||
|
||||
%>%install
|
||||
mkdir -p %{buildroot}/usr/
|
||||
cp -R "%{_topdir}/BUILD/usr/." "%{buildroot}/usr/"
|
||||
|
||||
|
||||
%files
|
||||
/usr/bin/<%= name %>
|
||||
/usr/lib/<%= name %>/
|
||||
/usr/share/applications/<%= name %>.desktop
|
||||
/usr/share/doc/<%= name %>/
|
||||
<% if (_.isObject(icon)) {
|
||||
_.forEach(icon, function (iconPath, resolution) {
|
||||
%>/usr/share/icons/hicolor/<%= resolution %>/apps/<%= name %><%= resolution === 'symbolic' ? '-symbolic' : '' %>.<%= ['scalable', 'symbolic'].includes(resolution) ? 'svg' : 'png' %>
|
||||
<% }) } else {
|
||||
%>/usr/share/pixmaps/<%= name %>.png
|
||||
<% } %>
|
||||
|
||||
<% if (pre) {
|
||||
%>%pre
|
||||
<% print(pre)
|
||||
if (preun || post || postun) print('\n\n\n') }
|
||||
|
||||
|
||||
%><% if (preun) {
|
||||
%>%preun
|
||||
<% print(preun)
|
||||
if (post || postun) print('\n\n\n') }
|
||||
|
||||
|
||||
%><% if (post) {
|
||||
%>%post
|
||||
<% print(post)
|
||||
if (postun) print('\n\n\n') }
|
||||
|
||||
|
||||
%><% if (postun) {
|
||||
%>%postun
|
||||
<% print(postun) } %>
|
||||
@@ -0,0 +1,84 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>TRIMSP-NL Quick Start</title>
|
||||
<link rel="stylesheet" href="ZGUI.css">
|
||||
</head>
|
||||
<body class="quick-start-document">
|
||||
<main class="quick-start-body" id="quickStartBody">
|
||||
<section class="quick-start-section" id="helpLayers">
|
||||
<h3>Layers and simulation</h3>
|
||||
<ol>
|
||||
<li>Choose the output folder and a descriptive file prefix.</li>
|
||||
<li>Set the number of layers, then enter each composition, density, and thickness.</li>
|
||||
<li>Select the projectile and define its energy, angle, spread, random-number generator, and seed.</li>
|
||||
<li>Choose <b>Run simulation</b>. Progress and the current state remain visible in the status bar.</li>
|
||||
</ol>
|
||||
<p>Known material densities are filled automatically. A highlighted density is an estimate and should be checked before running.</p>
|
||||
</section>
|
||||
|
||||
<section class="quick-start-section" id="helpScans">
|
||||
<h3>Energy and parameter scans</h3>
|
||||
<p>Enable the scan, select its parameter, and choose either a regular range or an explicit set of values. The summary reports the number of points and workers before the run starts.</p>
|
||||
<p>Electron can run scan points in parallel. The thread setting is local to the computer and is intentionally not stored in configuration files or shared links. The web backend executes scan points sequentially.</p>
|
||||
</section>
|
||||
|
||||
<section class="quick-start-section" id="helpResults">
|
||||
<h3>Results and output files</h3>
|
||||
<p>Profiles and layer fractions appear as scan points finish. Completed data are sorted by scan value, so parallel jobs may finish in any order without changing the final plots.</p>
|
||||
<p>In the web application, use <b>Outputs</b> in the status bar to download the result archive and <b>Details</b> to inspect CGI and engine diagnostics.</p>
|
||||
</section>
|
||||
|
||||
<section class="quick-start-section" id="helpAdvanced">
|
||||
<h3>Advanced transport parameters</h3>
|
||||
<p>These controls select collision, recoil, interaction-potential, and stopping-power models used by TRIM.SP-NL. Hover over a parameter name for its description and leave the defaults unchanged unless the simulation requires a specific model.</p>
|
||||
<p>The reference panel identifies the underlying TRIM.SP literature and the preferred citation for published work.</p>
|
||||
</section>
|
||||
|
||||
<section class="quick-start-section" id="helpSharing">
|
||||
<h3>Sharing a setup</h3>
|
||||
<p>The share button copies a link containing the current layers, projectile, scan, RNG, and advanced settings. Opening the link restores those inputs in the web application.</p>
|
||||
<p>Output folders, file prefixes, worker counts, logs, and generated data are never included. The recipient chooses their own output location before running.</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="quick-start-footer" id="quickStartFooter">
|
||||
<span>Questions? Contact <a href="mailto:zaher.salman@psi.ch">Zaher Salman</a>.</span>
|
||||
</footer>
|
||||
<script>
|
||||
(function() {
|
||||
function focusSection(sectionId) {
|
||||
let body = document.getElementById('quickStartBody');
|
||||
if (!body) return;
|
||||
for (let section of body.querySelectorAll('.quick-start-section')) {
|
||||
section.classList.remove('quick-start-section-active');
|
||||
}
|
||||
let target = document.getElementById(sectionId || 'helpLayers');
|
||||
if (!target) target = document.getElementById('helpLayers');
|
||||
if (!target) return;
|
||||
target.classList.add('quick-start-section-active');
|
||||
setTimeout(function() {
|
||||
body.scrollTop = Math.max(0, target.offsetTop - 8);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
focusSection(window.location.hash ? window.location.hash.slice(1) : 'helpLayers');
|
||||
window.addEventListener('hashchange', function() {
|
||||
focusSection(window.location.hash ? window.location.hash.slice(1) : 'helpLayers');
|
||||
});
|
||||
|
||||
if (typeof require === 'function') {
|
||||
try {
|
||||
require('electron').ipcRenderer.on('quickStartSection', function(event, payload) {
|
||||
focusSection(payload && payload.sectionId ? payload.sectionId : 'helpLayers');
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('Could not attach Quick Start Electron channel:', error);
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright (C) 2026 Zaher Salman
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
web_target="${TRIMSP_WEB_TARGET:-/var/www/html/TRIMSP-NL}"
|
||||
cgi_target="${TRIMSP_CGI_TARGET:-/var/www/cgi-bin/singleTrimSP.cgi}"
|
||||
trimsp_binary="${TRIMSP_BIN_PATH:-/usr/local/bin/trimspNL}"
|
||||
web_owner="${TRIMSP_WEB_OWNER:-apache}"
|
||||
web_group="${TRIMSP_WEB_GROUP:-apache}"
|
||||
plotly_source="$project_root/node_modules/plotly.js-dist/plotly.js"
|
||||
|
||||
if [[ ! -x "$trimsp_binary" ]]; then
|
||||
printf 'Installed TRIM.SP-NL binary is not executable: %s\n' "$trimsp_binary" >&2
|
||||
printf 'Install it first or set TRIMSP_BIN_PATH.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$plotly_source" ]]; then
|
||||
printf 'Plotly browser bundle not found: %s\n' "$plotly_source" >&2
|
||||
printf 'Run npm install before deploying.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
perl -c "$project_root/singleTrimSP.cgi" >/dev/null
|
||||
|
||||
# Install only files used by the browser application. Electron-specific code,
|
||||
# package metadata, tests, and Fortran sources stay outside the document root.
|
||||
install -d -m 0755 "$web_target" "$(dirname "$cgi_target")"
|
||||
install -m 0644 \
|
||||
"$project_root/TrimSP.html" \
|
||||
"$project_root/quick-start.html" \
|
||||
"$project_root/TrimSPlib.js" \
|
||||
"$project_root/TrimSPweb.js" \
|
||||
"$project_root/myplots.js" \
|
||||
"$project_root/ZGUI.css" \
|
||||
"$project_root/plotly.css" \
|
||||
"$project_root/icon.png" \
|
||||
"$web_target/"
|
||||
install -m 0644 "$plotly_source" "$web_target/plotly.js"
|
||||
ln -sfn TrimSP.html "$web_target/index.html"
|
||||
|
||||
# Keep the repository CGI generic while embedding the selected installed
|
||||
# binary path in the deployed copy.
|
||||
cgi_stage="$(mktemp)"
|
||||
trap 'rm -f "$cgi_stage"' EXIT
|
||||
escaped_trimsp_binary="${trimsp_binary//\\/\\\\}"
|
||||
escaped_trimsp_binary="${escaped_trimsp_binary//&/\\&}"
|
||||
escaped_trimsp_binary="${escaped_trimsp_binary//|/\\|}"
|
||||
escaped_trimsp_binary="${escaped_trimsp_binary//\"/\\\"}"
|
||||
sed 's|^my \$trimsp_bin = .*|my $trimsp_bin = "'"$escaped_trimsp_binary"'";|' \
|
||||
"$project_root/singleTrimSP.cgi" > "$cgi_stage"
|
||||
install -m 0755 "$cgi_stage" "$cgi_target"
|
||||
|
||||
if getent passwd "$web_owner" >/dev/null 2>&1 && getent group "$web_group" >/dev/null 2>&1; then
|
||||
chown -R "$web_owner:$web_group" "$web_target"
|
||||
chown "$web_owner:$web_group" "$cgi_target"
|
||||
else
|
||||
printf 'Warning: owner/group %s:%s does not exist; ownership was not changed.\n' \
|
||||
"$web_owner" "$web_group" >&2
|
||||
fi
|
||||
|
||||
if command -v restorecon >/dev/null 2>&1; then
|
||||
restorecon -RF "$web_target" "$cgi_target" || true
|
||||
fi
|
||||
|
||||
printf 'Installed TRIMSP-NL web assets in %s\n' "$web_target"
|
||||
printf 'Installed TRIMSP-NL CGI runner at %s\n' "$cgi_target"
|
||||
printf 'Using installed TRIM.SP-NL engine at %s\n' "$trimsp_binary"
|
||||
printf '\nThe web server must provide:\n'
|
||||
printf ' /cgi-bin/singleTrimSP.cgi -> %s\n' "$cgi_target"
|
||||
printf 'For SELinux deployments, verify CGI execution and write access with:\n'
|
||||
printf ' ausearch -m avc -ts recent\n'
|
||||
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/perl
|
||||
#
|
||||
# Copyright Zaher Salman 2023-.
|
||||
# zaher.salman@psi.ch
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use CGI;
|
||||
use File::Path qw(make_path remove_tree);
|
||||
use File::Temp qw(tempfile);
|
||||
use JSON::PP qw(encode_json);
|
||||
|
||||
# The web installer rewrites this path to the selected installed binary.
|
||||
my $trimsp_bin = "/usr/local/bin/trimspNL";
|
||||
my $workroot = "/tmp";
|
||||
|
||||
my $in = CGI->new;
|
||||
my $action = $in->param("action") // "run";
|
||||
|
||||
if ($action eq "read") {
|
||||
my $read_filename = $in->param("fn") // "";
|
||||
if ($read_filename !~ m{^/tmp/[A-Za-z0-9_-]+/[A-Za-z0-9_.-]+$}
|
||||
|| !-f $read_filename) {
|
||||
print "Status: 404 Not Found\r\n";
|
||||
print "Content-Type: text/plain; charset=UTF-8\r\n\r\n";
|
||||
print "Requested simulation output was not found.\n";
|
||||
exit 0;
|
||||
}
|
||||
|
||||
open(my $input_file, "<", $read_filename) or do {
|
||||
print "Status: 500 Internal Server Error\r\n";
|
||||
print "Content-Type: text/plain; charset=UTF-8\r\n\r\n";
|
||||
print "Requested simulation output could not be read.\n";
|
||||
exit 0;
|
||||
};
|
||||
print "Content-Type: text/plain; charset=UTF-8\r\n\r\n";
|
||||
while (read($input_file, my $buffer, 65536)) {
|
||||
print $buffer;
|
||||
}
|
||||
close($input_file);
|
||||
exit 0;
|
||||
}
|
||||
|
||||
if ($action eq "download") {
|
||||
my $download_run = $in->param("run") // "";
|
||||
my $archive_path = "$workroot/$download_run.tgz";
|
||||
if ($download_run !~ /^[A-Za-z0-9_-]+$/ || !-f $archive_path) {
|
||||
print "Status: 404 Not Found\r\n";
|
||||
print "Content-Type: text/plain; charset=UTF-8\r\n\r\n";
|
||||
print "Requested results archive was not found.\n";
|
||||
exit 0;
|
||||
}
|
||||
|
||||
open(my $archive_file, "<", $archive_path) or do {
|
||||
print "Status: 500 Internal Server Error\r\n";
|
||||
print "Content-Type: text/plain; charset=UTF-8\r\n\r\n";
|
||||
print "Requested results archive could not be read.\n";
|
||||
exit 0;
|
||||
};
|
||||
binmode($archive_file);
|
||||
binmode(STDOUT);
|
||||
print "Content-Type: application/gzip\r\n";
|
||||
print "Content-Disposition: attachment; filename=\"$download_run.tgz\"\r\n";
|
||||
print "Content-Length: " . (-s $archive_path) . "\r\n\r\n";
|
||||
while (read($archive_file, my $buffer, 65536)) {
|
||||
print $buffer;
|
||||
}
|
||||
close($archive_file);
|
||||
exit 0;
|
||||
}
|
||||
|
||||
my $requested_filename = $in->param("fn") // "";
|
||||
my $rng_type = $in->param("rngType") // "pcg32";
|
||||
|
||||
# Accept only the private run path produced by TrimSPweb.js. Restricting both
|
||||
# path components prevents CGI parameters from selecting arbitrary files.
|
||||
if ($requested_filename !~ m{^/tmp/([A-Za-z0-9_-]+)/([A-Za-z0-9_.-]+)$}) {
|
||||
print "Status: 400 Bad Request\r\n";
|
||||
print "Content-Type: application/json\r\n\r\n";
|
||||
print encode_json({
|
||||
ok => JSON::PP::false,
|
||||
phase => "request",
|
||||
message => "Invalid simulation filename",
|
||||
});
|
||||
exit 0;
|
||||
}
|
||||
|
||||
my ($run_name, $file_name) = ($1, $2);
|
||||
$rng_type = "pcg32" unless $rng_type =~ /^(?:ranlux|pcg32|xoshiro|xoshiro256)$/;
|
||||
|
||||
my $work_path = "$workroot/$run_name";
|
||||
my $filename = "$work_path/$file_name";
|
||||
my $run_base = $file_name;
|
||||
$run_base =~ s/\.inp$//;
|
||||
|
||||
# Reconstruct submitted file content in numeric line order. CGI parameter
|
||||
# iteration order is not guaranteed for line10, line11, and later fields.
|
||||
my @line_parameters = sort {
|
||||
($a =~ /line(\d+)/)[0] <=> ($b =~ /line(\d+)/)[0]
|
||||
} grep { /^line\d+$/ } $in->param;
|
||||
my $content = join("\n", map { scalar($in->param($_) // "") } @line_parameters) . "\n";
|
||||
|
||||
make_path($work_path, { mode => 0755 }) unless -d $work_path;
|
||||
open(my $output_file, ">", $filename) or do {
|
||||
print "Status: 500 Internal Server Error\r\n";
|
||||
print "Content-Type: application/json\r\n\r\n";
|
||||
print encode_json({
|
||||
ok => JSON::PP::false,
|
||||
phase => "write",
|
||||
message => "Could not write the submitted simulation file",
|
||||
});
|
||||
exit 0;
|
||||
};
|
||||
print {$output_file} $content;
|
||||
close($output_file);
|
||||
|
||||
my $phase = "save";
|
||||
my $message = "Saved $file_name";
|
||||
my $exit_code;
|
||||
my $archive_url;
|
||||
my $log_content = "";
|
||||
my $request_logfile;
|
||||
my $ok = JSON::PP::true;
|
||||
|
||||
if ($filename =~ /_Seq_Results\.dat$/) {
|
||||
$phase = "archive";
|
||||
unlink("$work_path/edist");
|
||||
|
||||
# Only simulation files live in the run directory. Diagnostic output is
|
||||
# captured separately and is therefore excluded from the archive.
|
||||
my $archive_path = "$workroot/$run_name.tgz";
|
||||
my ($log_handle, $logfile) = tempfile("trimsp-$run_name-XXXXXX", DIR => $workroot, UNLINK => 0);
|
||||
close($log_handle);
|
||||
$request_logfile = $logfile;
|
||||
my $command = "tar -czf '$archive_path' -C '$work_path' . > '$logfile' 2>&1";
|
||||
system($command);
|
||||
$exit_code = $? == -1 ? 127 : $? >> 8;
|
||||
if ($exit_code == 0) {
|
||||
$archive_url = "/cgi-bin/singleTrimSP.cgi?action=download&run=$run_name";
|
||||
$message = "Created results archive";
|
||||
remove_tree($work_path);
|
||||
} else {
|
||||
$ok = JSON::PP::false;
|
||||
$message = "Could not create results archive";
|
||||
}
|
||||
} elsif ($filename !~ /TrimSP\.cfg$/) {
|
||||
$phase = "simulation";
|
||||
my ($log_handle, $logfile) = tempfile("trimsp-$run_name-XXXXXX", DIR => $workroot, UNLINK => 0);
|
||||
close($log_handle);
|
||||
$request_logfile = $logfile;
|
||||
|
||||
# All user-derived command components were restricted above. Each request
|
||||
# receives a private log file so concurrent users never share output.
|
||||
my $command = "cd '$work_path' && timeout 300 '$trimsp_bin' "
|
||||
. "'$run_base' '$rng_type' >> '$logfile' 2>&1";
|
||||
system($command);
|
||||
$exit_code = $? == -1 ? 127 : $? >> 8;
|
||||
$ok = JSON::PP::false if $exit_code != 0;
|
||||
$message = $exit_code == 0
|
||||
? "TRIM.SP-NL completed $run_base"
|
||||
: "TRIM.SP-NL failed while running $run_base";
|
||||
}
|
||||
|
||||
# Return at most the latest 64 KiB to the browser and immediately remove the
|
||||
# temporary server-side copy. Diagnostic logs never enter the results archive.
|
||||
if ($request_logfile && -f $request_logfile && open(my $log_handle, "<", $request_logfile)) {
|
||||
my $log_size = -s $log_handle;
|
||||
seek($log_handle, $log_size - 65536, 0) if $log_size > 65536;
|
||||
local $/;
|
||||
$log_content = <$log_handle> // "";
|
||||
close($log_handle);
|
||||
unlink($request_logfile);
|
||||
}
|
||||
|
||||
print "Content-Type: application/json\r\n\r\n";
|
||||
print encode_json({
|
||||
ok => $ok,
|
||||
phase => $phase,
|
||||
message => $message,
|
||||
exitCode => $exit_code,
|
||||
archiveUrl => $archive_url,
|
||||
log => $log_content,
|
||||
});
|
||||
|
||||
exit 0;
|
||||