Files
TRIMSP/TrimSPweb.js
T

233 lines
8.1 KiB
JavaScript

// 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 = 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.set("rngType", rngType.value);
}
let lines = content.split(/\n/);
let prefix = filename.split(/\//);
for (let i=0; i<lines.length; i++) {
cgiargs.set("line" + i, lines[i].replace(/\s\s+/g, ' '));
}
var xhttp;
xhttp = new XMLHttpRequest();
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.toString()); //POST
let response = null;
try {
response = JSON.parse(xhttp.responseText);
} catch (error) {
console.error("Invalid CGI response", error, xhttp.responseText);
}
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);
}
if (response.archiveUrl) {
let outputLink = document.getElementById("statusOutputLink");
outputLink.href = response.archiveUrl;
outputLink.textContent = "Outputs";
outputLink.title = `Download ${prefix[2]}.tgz`;
outputLink.hidden = false;
}
return(1);
}
function readAsciiFile(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;
}
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);
}