Sindbad~EG File Manager
<?php
header('Content-Type: text/html; charset=UTF-8');
// Set HTTP header to prevent caching
header("Cache-Control: no-cache, must-revalidate");
if ($_SERVER['SERVER_NAME'] == 'localhost') {
$serverAddress = 'localhost';
$username = $password = null;
set_time_limit(3600); // Allow the script to run for up to 3600 seconds for debugging
} else {
$serverAddress = 'mathxpert.org';
$username = 'beeson';
$password = 'Turing2024';
}
ini_set('display_errors', 1);
ini_set('session.cookie_samesite', 'None');
ini_set('session.cookie_secure', '1'); // Required for SameSite=None
// Start or resume the session
// Ensure we're using the correct session ID before starting the session
if (isset($_POST["PHPSESSID"])) {
session_id($_POST["PHPSESSID"]); // Use the session ID from the form
}
session_start();
$sessionId = session_id(); // guaranteed not to contain a pipe character
error_reporting(E_ALL);
$serverPort = 12349; // Adjust the server port. Ending in 9 for the Engine; in 7 for Polygon
$timeout = 3600; // Connection timeout in seconds. If the server does not respond by then, close the socket.
$startupDelay = 5; // Delay for server startup in seconds if the server is not already running
$imageBaseDir = "/Users/beeson/Dropbox/MathXpert/images/";
if (!(isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] == 'on' || $_SERVER['HTTPS'] == 1) || isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https')) {
$redirect = 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
header('HTTP/1.1 301 Moved Permanently');
header('Location: ' . $redirect);
exit();
}
require("SendMessage.php");
$clientSocket = createClientSocket($serverAddress, $serverPort, $timeout);
// Handle Ajax requests before emitting HTML
if ($_SERVER['REQUEST_METHOD'] === 'POST')
{ $response = "nothing";
if ( isset($_POST['graphTitleMovedParam']))
{
$param = $_POST['graphTitleMovedParam'];
// Process the AJAX request
//$response = sendMessage($clientSocket,"graphTitleMoved",$param);
// We do not care what the response is as long as it is not false
}
if ( isset($_POST['askPointSlopeParam']))
{
$param = $_POST['askPointSlopeParam'];
// Process the AJAX request
$pointSlopeResponse = sendMessage($clientSocket,"askPointSlope",$param);
echo($pointSlopeResponse);
exit; // without this, the entire document GetProb.php produces with this $_POST
// would be sent to the listener waiting for the Ajax result.
}
else if ($response != "nothing")
{ // $response is the response to the Ajax request
// echo($response); // this will display an error message for the developer if an incorrect parameter was sent.
exit; // Stop further processing, so the rest of the HTML isn't sent
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, maximum-scale=1.0, user-scalable=0">
<title>MathXpert Grapher</title>
<style>
body {
user-select: none;
overflow: hidden; /* Disable scrollbars for the entire page */
}
body, html {
margin: 0;
padding: 0;
height: 100%;
border: none;
}
/* Set a global font-family rule for all text elements. Only Times New Roman actually works well. */
text {
font-family: 'Times New Roman', 'STIX Two Math', 'Cambria Math', 'Latin Modern Math', 'TeX Gyre Termes Math';
}
svg text {
font-family: 'Times New Roman';
}
/* Ensure Arial for text inside elements with the class svg-button */
svg-button text, .svg-button svg text {
font-family: 'Arial';
}
/* the following container with horizontal flow will hold the Toolbar and the svgContainer,
so the svgContainer will come to the right of the Toolbar */
.container {
display: flex;
height: 100%;
align-items: flex-start; /* begin each column at the top */
/* the first column contains the Toolbar, so this gets the Toolbar at the top.
position:relative; */
top: 0;
left: 0;
}
/* Define svgContainer div so it comes to the right of the Toolbar.
We have to have either absolute or relative positioning here so that the SVG output of the Engine will be
interpreted relative to svgContainer. If we use position:absolute, then we must also specify left and top.
Using postion:relative allows flex layout in .container to place svgContainer to the
right of the Toolbar without hardcoding the width of the Toolbar.
*/
#svgContainer {
position:relative;
overflow: hidden; /* Prevent scrollbars appearing when there's a wheel event */
z-index: 1;
text-shadow: none;
}
.popup svg {
width: 100%;
height: auto;
word-wrap: break-word;
white-space: pre-wrap;
}
#titleRect {
position: absolute;
cursor: move;
border: none;
background-color: transparent;
text-shadow: none;
}
[id^="draggableTitle"] { /* applies to draggableTitle0, draggableTitle1, etc. */
position: absolute;
cursor: move;
border: 1px solid rgb(0,0,128);
background-color: transparent; // will be overwritten with inline style
display: none; // will be changed when used;
text-shadow: none;
padding: 5px;
color: black;
top: 10px;
left: 10px; /* Initial position */
z-index: 2; /* Ensure it's above other elements */
}
#PointSlopeDiv {
position: absolute;
cursor: move;
border: 5px;
background-color: transparent;
border-color: transparent;
text-shadow: none;
padding: 5px;
color: black;
top: 50px;
left: 50px; /* Initial position */
width:auto;
height:auto;
z-index: 2; /* Ensure it's above other elements */
}
#crosshairs-container {
position: absolute;
top: 0;
left: 0;
display:none;
background-color:transparent;
pointer-events: none; /* Allow click events to pass through */
}
#selectionRectangle {
position: absolute;
border: 1px solid red;
pointer-events: none; /* Ignore this element during mouse events */
}
body {
background-color:none;
}
/* Style for the modal */
#hintModal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0);
justify-content: center;
align-items: center;
}
/* Style for the modal content */
#modalContent {
background: rgb(200,200,255);
padding: 20px;
border-radius: 5px;
width: 50%;
height: 50%;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);
}
</style>
<style>
.popup-container {
position: absolute;
background-color: lightblue;
border: 1px solid black;
padding: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
z-index: 5000; /* Ensure it appears above other elements */
display: flex;
flex-direction: column;
align-items: center; /* align labels and sliders */
width: 180px; /* Set a fixed width to prevent resizing and flicker */
pointer-events: all; /* Make sure the popup can capture events */
}
.popup-container label {
margin-bottom: 5px;
text-align:left;
width: 100%;
display: block;
background-color: lightblue;
}
.popup-container input[type="range"] {
width: 100%;
margin-bottom: 10px;
pointer-events: all;
}
.popup-container button {
display: block;
margin: 0 auto;
background-color:lightyellow !important;
}
</style>
<style>
#customContextMenu {
font-family: Arial, sans-serif;
}
</style>
<style>
.ODEhint {
position: absolute;
top: 10px;
left: 10px;
padding: 5px 10px;
background-color: rgba(255, 255, 255, 0.8);
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
color: #333;
pointer-events: none; /* Allow clicks to pass through */
opacity:0
/*transition: opacity 0.5s ease; */
}
</style>
<style>
/* Default white knob (inactive slider) */
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 20px;
height: 20px;
background: white;
border: 2px solid black;
border-radius: 50%;
cursor: pointer;
}
/* Lighter green knob for the active slider */
input[type="range"].active-slider::-webkit-slider-thumb {
background: #90EE90; /* Light green (a softer contrast than dark green) */
}
/* Firefox */
input[type="range"]::-moz-range-thumb {
width: 20px;
height: 20px;
background: white;
border: 2px solid black;
border-radius: 50%;
cursor: pointer;
}
input[type="range"].active-slider::-moz-range-thumb {
background: #90EE90; /* Light green */
}
</style>
</head>
<?php
if (!(isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] == 'on' ||
$_SERVER['HTTPS'] == 1) ||
isset($_SERVER['HTTP_X_FORWARDED_PROTO']) &&
$_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https'))
{
$redirect = 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
header('HTTP/1.1 301 Moved Permanently');
header('Location: ' . $redirect);
exit();
}
?>
<body>
<script src="DocumentID.js"></script>
<script src="TranslateEnterPages.js"></script>
<script src="GraphPaperButton.js"></script>
<script src="Keyboard.js"></script>
<script src="FetchMessage.js"></script>
<script src="AdjustParameter.js"></script>
<script src="ParameterSlider.js"></script>
<script src="ContextMenu.js"></script>
<?php
function Olddecode($response,$fillcolor){
// To save bandwidth we've compressed some SVG commands, now expand them
// This only saves bandwidth between the Engine and the PHP code, not between the web server and browser,
// but otherwise the 1 megabyte limit on messages between the Engine and PHP is broken, i.e., the length of
// messages needed to draw these graphs is more than 6 digits. It's easier
// just to do this decoding than alter the basic Engine message mechanisms.
$pattern = '/<q (-?\d+) (-?\d+)>/';
return preg_replace_callback(
$pattern,
function ($matches) use ($fillcolor) { // Use "use" to inherit $fillcolor
$x = $matches[1] - 0.5;
$y = $matches[2] - 0.5;
return "<circle cx=\"{$x}\" cy=\"{$y}\" r=\"1\" style=\"fill:{$fillcolor};stroke:none;\" />\n";
},
$response
);
}
// following version of decode avoids regular expression matching, which uses too much memory.
// it echoes the result rather than returning it.
function decode($response, $fillcolor) {
// Initialize the output buffer
$output = '';
// Define the tag we're looking for
$tag = '<q ';
$tagLength = strlen($tag);
// Position in the string
$pos = 0;
$length = strlen($response);
// Loop through the response and process each occurrence of the tag
while (($start = strpos($response, $tag, $pos)) !== false) {
// Output the part of the response before the tag
echo (substr($response, $pos, $start - $pos));
// Find the closing '>'
$end = strpos($response, '>', $start);
if ($end === false) {
// If no closing '>', stop processing and output the rest
echo(substr($response, $start));
break;
}
// Extract the coordinates
$coords = substr($response, $start + $tagLength, $end - $start - $tagLength);
list($x, $y) = explode(' ', $coords);
// Adjust the coordinates
$x = $x - 0.5;
$y = $y - 0.5;
// Add the decoded circle element to the output
echo ("<circle cx=\"{$x}\" cy=\"{$y}\" r=\"1\" style=\"fill:{$fillcolor};stroke:none;\" />\n");
// Move the position past this tag
$pos = $end + 1;
}
// Output the remaining part of the response (after the last tag)
if ($pos < $length) {
echo (substr($response, $pos));
}
}
function decode2($response){
$pattern2 = '/<Q (-?\d+) (-?\d+) (\d+) (\d+)\s*\/>/';
return preg_replace_callback(
$pattern2,
function ($matches) {
$x = $matches[1];
$y = $matches[2];
$w = $matches[3];
$h = $matches[4];
return "<rect x=\"{$x}\" y=\"{$y}\" width=\"{$w}\" height=\"{$h}\" />\n";
},
$response
);
}
?>
<?php
if ($_SERVER["REQUEST_METHOD"] === "POST")
{ if(isset($_SESSION['windowWidth']))
// in case width and height don't get set below, let's make sure they are set.
$width = $windowWidth = $_SESSION['windowWidth'] ;
if(isset($_SESSION['graphWidth']))
$graphwidth = $_SESSION['graphWidth'];
if(isset($_SESSION['height']))
$height = $_SESSION['height'];
$fillcolor = "rgb(128,128,255)";
if(isset($_SESSION['toolbarwidth']))
{ $toolbarwidth = $_SESSION['toolbarwidth']; // it's 66 at this point
}
if (isset($_POST['problemnumberField']))
// this means the problemsource is SOURCE_MATHXPERT, as typed-in problems don't have numbers
{
$problemNumber = intval($_POST["problemnumberField"]);
$topicNumber = intval($_POST["topicField"]);
$sender = "GetProblem";
$language = $_POST["language"];
$graphwidth = $_SESSION['graphWidth'] = intval($_POST["widthField"]); // width of the browser window minus toolbar
$toolbarwidth = $_SESSION['toolbarwidth'] = intval($_POST["toolbarwidthField"] );
$height = $_SESSION['height'] = intval($_POST["heightField"]); // height of the browser window
$_SESSION['height'] = $height;
$_SESSION['topicNumber'] = $topicNumber;
$_SESSION['problemNumber'] = $problemNumber;
}
if (isset($_POST["widthField2"])) // 'graphButton_x' doesn't work!
{
$sender = "graphButton";
$graphwidth = $_SESSION['graphWidth'] = intval($_POST["widthField2"]);
$toolbarwidth = $_SESSION['toolbarwidth'] = intval($_POST["toolbarwidthField"]);
$height = $_SESSION['height'] = intval($_POST["heightField2"]);
$windowWidth = $graphwidth + $toolbarwidth;
$width = $windowWidth;
// echo("width = $width and toolbarwidth = $toolbarwidth and graphwidth = $graphwidth");
$language = $_POST["language"];
$sessionID = $_POST["PHPSESSID"];
}
if (isset($_POST["widthField3"])) // from EnterOrdinaryGraph.php or other EnterSomething.php files
{
$sender = "drawAll";
$graphwidth = $_SESSION['graphWidth'] = intval($_POST["widthField3"]);
$toolbarwidth = $_SESSION['toolbarwidth'] = intval($_POST["toolbarwidthField"]);
$height = $_SESSION['height'] = intval($_POST["heightField3"]);
$windowWidth = $graphwidth + $toolbarwidth;
$width = $windowWidth;
// echo("width = $width and toolbarwidth = $toolbarwidth and graphwidth = $graphwidth");
$language = $_POST["language"];
}
if (isset($_POST['language2']))
{
$sender = "setLanguage";
$language = $languageName = $_POST['language2'];
}
if (isset($_POST['languageName']))
{
$sender = "setLanguage";
$languageName = $_POST['languageName'];
}
if (isset($_POST['action'])) {
$sender = $_POST['action'];
// Now, $sender should contain something like "parameterplus0"
}
if(isset($_POST['newActiveParameter']))
{
$sender = "activeParameterChanged";
$newActiveParameter = $_POST['newActiveParameter'];
}
$resetSliderColors = isset($_POST['newActiveParameter']);
if (isset($_POST['horizontalzoomout']))
{
$sender = "horizontalzoomout";
}
if (isset($_POST['horizontalzoomin']))
{
$sender = "horizontalzoomin";
}
if (isset($_POST['verticalzoomout']))
{
$sender = "verticalzoomout";
}
if (isset($_POST['verticalzoomin']))
{
$sender = "verticalzoomin";
}
if (isset($_POST['doublezoomout']))
{
$sender = "doublezoomout";
}
if (isset($_POST['doublezoomin']))
{
$sender = "doublezoomin";
}
if ( isset($_POST['zoomAtPointParam']))
{
$param = $_POST['zoomAtPointParam'];
$sender = "zoomAtPoint";
}
if ( isset($_POST['graphMovedParam']))
{
$param = $_POST['graphMovedParam'];
$sender = "graphMoved";
$_SESSION['mouseFunction'] = 0; // mouse will do nothing by default
// You must click something to make the mouse do something
// scrolling default made it difficult to resize the window without moving the graph
}
if (isset($_POST['selectedRectangleField']) && !empty($_POST['selectedRectangleField']))
{
$selectedRectangleParameter = $_POST['selectedRectangleField'];
$_SESSION['mouseFunction'] = $_POST['mouseFunctionField'];
// so it can be restored after Draw causes sendGraphDocumnt
$sender = "draw";
}
if (isset($_POST['graphWindowResizedField']))
{ $param = $_POST['graphWindowResizedField'];
$sender = "graphWindowResized";
$newgraphwidth = $_POST['graphwidth'];
$_SESSION['graphWidth'] = $newgraphwidth;
$_SESSION['height'] = $_POST['newHeight'];
}
if(isset($_POST['directionField']))
{
$sender = "toggleDirectionField";
}
if(isset($_POST['circularAspect']))
{
$sender = "circularAspect";
}
if(isset($_POST['graphPaperIndexFieldName']))
{
$sender = "setGraphPaper";
$index = $_POST['graphPaperIndexFieldName'];
$param = $graphwidth . "+" . $height . "+" . $index;
}
if(isset($_POST['parametersField']))
{
$sender = "updateParameters";
$param = $graphwidth . "+" . $height . "+" . $_POST['parametersField'];
// See AdjustParameter.js for the format of the POSTed data.
}
if(isset($_POST['newStrokeWidth']))
{
// from the context menu of a graph
$thickness = $_POST['newStrokeWidth']; // it will be 1 or 3 as a string
$param = $graphwidth . "+" . $height . "+" . $thickness;
$sender = "changeGraphLineWidth";
}
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($socket === false)
{
echo "Socket creation failed: " . socket_strerror(socket_last_error()) . "<br>";
}
else
{
socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, array("sec" => $timeout, "usec" => 0));
$result = socket_connect($socket, $serverAddress, $serverPort);
if(isset($_SESSION['graphWidth']))
{
$graphwidth = $_SESSION['graphWidth'];
$windowWidth = $graphwidth + $toolbarwidth;
}
if(isset($_SESSION['height']))
$height = $_SESSION['height'];
// echo("$windowWidth $toolbarwidth $graphwidth");
$fillcolor = "rgb(128,128,255)";
if ($result)
{
if($sender=="GetProblem")
{ $param = $topicNumber . "+" . $problemNumber . "+" . $language . "+" . $graphwidth . "+" . $height;
unset($_SESSION['mouseFunction']);
$response = sendMessage($clientSocket, "initGraphDocFromLibrary", $param);
}
else if ($sender == "graphButton")
{ // echo("sending graphButton message");
$param = $graphwidth . "+" . $height;
$response = sendMessage($clientSocket,"graphButton", $param);
}
else if($sender == "setLanguage")
{
$param = $languageName;
}
else if( str_starts_with($sender, "parameterplus"))
{
$index = $sender[13]; // the character after parameterplus
$param = $graphwidth . "+" . $height. "+" . $index;
$response = sendMessage($clientSocket,"incrementParameter", $param);
// this also resets pDocData->active_parameter in the Engine.
}
else if( str_starts_with($sender, "parameterminus"))
{
$index = $sender[14]; // the character after parameterminus
$param = $graphwidth . "+" . $height. "+" . $index;
$response = sendMessage($clientSocket,"decrementParameter", $param);
}
else if ($sender == "activeParameterChanged")
{
$param ="$newActiveParameter+$graphwidth+$height";
$response = sendMessage($clientSocket,"activeParameterChanged",$param);
}
else if ($sender == "incrementActiveParameter")
{
$param ="$graphwidth+$height";
$response = sendMessage($clientSocket,"incrementActiveParameter",$param);
}
else if ($sender == "decrementActiveParameter")
{
$param ="$graphwidth+$height";
$response = sendMessage($clientSocket,"decrementActiveParameter",$param);
}
else if ($sender == "draw")
{
$param = $selectedRectangleParameter;
$response = sendMessage($clientSocket,"selectedRectangleGraph", $param);
}
else if ($sender == "drawAll")
{
$param = "$graphwidth+$height";
$response = sendMessage($clientSocket,"drawAll", $param);
}
else if ($sender == "zoomAtPoint")
{
$response = sendMessage($clientSocket,"zoomAtPoint", $param);
}
else if ($sender == "graphMoved")
{
$response = sendMessage($clientSocket,"graphMoved", $param);
}
else if ($sender == "graphWindowResized")
{
$response = sendMessage($clientSocket,"graphWindowResized", $param);
}
else if ($sender == "setGraphPaper")
{
$response = sendMessage($clientSocket,"setGraphPaper", $param);
}
else if ($sender == "updateParameters")
{
$response = sendMessage($clientSocket,"updateParameters", $param);
}
else if ($sender == "changeGraphLineWidth")
{
$response = sendMessage($clientSocket,"changeGraphLineWidth", $param);
}
else
{
if(!is_string($sender) || strlen($sender) == 0)
$response = false;
else
{
$param = $graphwidth . "+" . $height;
$response = sendMessage($clientSocket,$sender,$param);
}
}
if ($response === false || $response == null)
{
$errcode = socket_last_error($socket);
$message = socket_strerror($errcode);
echo "Socket_read error: $message<br>";
}
else if (strpos($response, 'ERROR:') === 0)
{ // It's an error message
$errorMessage = substr($response, strlen('ERROR:'));
echo "<div class='error'>Error: {$errorMessage}</div>";
}
else
{ // The response consists of multiple <script> elements followed by <svg> elements
$position = 0;
while (($position = stripos($response, '</script>', $position)) !== false) {
$afterScript = substr($response, $position + strlen('</script>'), 5); // Check next 5 characters
if (stripos($afterScript, '<script') === false) {
// Found the first `</script>` that is NOT followed by another `<script>` within 5 chars
$position += strlen('</script>'); // Include `</script>` in the split
break;
}
$position += strlen('</script>'); // Move forward in the string
}
if ($position !== false) {
$scriptPart = substr($response, 0, $position);
$svgPart = substr($response, $position);
} else {
// If "</script>" is not found, something went wrong, possibly an error message
echo($response);
$scriptPart = '';
}
if (!strstr($scriptPart, "languageName")) die();
echo($scriptPart);
?>
<div class="container" id="wholeWindow">
<?php include 'toolbar.php'; ?>
<script>
setLanguage(languageName);
</script>
<script src="Tooltips.js"></script>
<script src="AdjustToolbar.js"></script>
<div id="svgContainer">
<div id="ODEhint" class="ODEhint">Click anywhere on the graph to start a new solution at that point.</div>
<?php
if(isset($_SESSION['ODEhint']))
echo("<script> show_ODEhint = false; </script>");
else
echo("<script> show_ODEhint = true; </script>");
?>
<div id="PointSlopeDiv" style="position: absolute; cursor: move; border: 5px; background-color: transparent; padding: 5px; left:50px; top:50px"></div>
<div id="crosshairs-container">
<svg width="38" height="38">
<!-- Vertical lines -->
<line x1="19" y1="0" x2="19" y2="18" stroke="black" stroke-width="1"/>
<line x1="19" y1="20" x2="19" y2="38" stroke="black" stroke-width="1"/>
<!-- Horizontal lines -->
<line x1="0" y1="19" x2="18" y2="19" stroke="black" stroke-width="1"/>
<line x1="20" y1="19" x2="38" y2="19" stroke="black" stroke-width="1"/>
</svg>
</div>
<div id="draggableTitle0" style="position: absolute; display:none; cursor: move; background-color: lightyellow; padding: 5px; left:10px; top:10px"></div>
<div id="draggableTitle1" style="position: absolute; display:none; cursor: move; background-color: lightyellow; padding: 5px; left:10px; top:10px"></div>
<div id="draggableTitle2" style="position: absolute; display:none; cursor: move; background-color: lightyellow; padding: 5px; left:10px; top:10px"></div>
<div id="draggableTitle3" style="position: absolute; display:none; cursor: move; background-color: lightyellow; padding: 5px; left:10px; top:10px"></div>
<div id="draggableTitle4" style="position: absolute; display:none; ursor: move; background-color: lightyellow; padding: 5px; left:10px; top:10px"></div>
<div id="draggableTitle5" style="position: absolute; display:none; cursor: move; background-color: lightyellow; padding: 5px; left:10px; top:10px"></div>
<svg id="graphSVG" width="<?php echo $graphwidth; ?>" height="<?php echo $height; ?>" top = "0px ?>"; xmlns="http://www.w3.org/2000/svg">
<?php
if(strstr($svgPart,"<q") || strstr($svgPart,"<Q"))
{
// on relation graphs, colored pixels are coded with <q and small rectangles with <Q
decode($svgPart,$fillcolor);
}
else
echo $svgPart;
?>
</svg>
</div> <!-- This closes the svgContainer div -->
</div> <!-- This closes the container div -->
<?php
}
socket_close($socket); // because server has already closed it
} // close if($result)
else
{
echo "Failed to connect to the C program: " . socket_strerror(socket_last_error()) . "<br>";
}
}
}
?>
<script src="wrapSVGtext.js"></script>
<script src="commentaryModal.js"></script>
<script src="crashPopup.js"></script>
<script src="MoveSvgElement.js"></script>
<script>
updateGraphPaperDisplay(); // initialize the GraphPaperButton
</script>
<script>
// The mouse can perform different functions. Which one it is
// used for is controlled by these variables, which are changed
// by buttons on the Graph Toolbar
const mouseScrollGraph = 1;
const mouseSelectRectangle = 2;
const mouseSelectCenteredRectangle = 3;
const mousePointSlope = 4;
const mouseDragTitle = 5;
let mouseFunction = <?php if(isset($_SESSION['mouseFunction'])) echo($_SESSION['mouseFunction']); else echo("0"); ?>;
let pointSlopeOn = false;
// Code to drag the titles
// Make the draggableTitle elements draggable (always, this can't be turned on or off)
dragElement(document.getElementById("draggableTitle0"));
dragElement(document.getElementById("draggableTitle1"));
dragElement(document.getElementById("draggableTitle2"));
dragElement(document.getElementById("draggableTitle3"));
dragElement(document.getElementById("draggableTitle4"));
dragElement(document.getElementById("draggableTitle5"));
// also the PointSlope window
dragElement(document.getElementById("PointSlopeDiv"));
function dragElement(elmnt) {
var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
var oldmouseFunction = mouseFunction;
let dragContainer = elmnt.parentElement;
let maxWidth = parseFloat(getComputedStyle(dragContainer).width);
let maxHeight = parseFloat(getComputedStyle(dragContainer).height);
if (document.getElementById(elmnt.id)) {
document.getElementById(elmnt.id).onpointerdown = dragMouseDown;
} else {
elmnt.onpointerdown = dragMouseDown;
}
function dragMouseDown(e) {
e.preventDefault();
e.stopPropagation();
pos3 = e.clientX;
pos4 = e.clientY;
//console.log("Pointer down at:", pos3, pos4);
document.onpointerup = closeDragElement;
document.onpointermove = elementDrag;
console.log("Pointer move listener attached.");
oldMouseFunction = mouseFunction;
mouseFunction = mouseDragTitle;
}
function elementDrag(e) {
e.preventDefault();
e.stopPropagation();
// Calculate the position delta
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
// New potential positions of the element
let newLeft = elmnt.offsetLeft - pos1;
let newTop = elmnt.offsetTop - pos2;
// Prevent dragging off the left
if (newLeft < 0) {
newLeft = 0;
}
// Prevent dragging off the top
if (newTop < 0) {
newTop = 0;
}
let newRight = newLeft + elmnt.offsetWidth;
let newBottom = newTop + elmnt.offsetHeight;
// Prevent dragging off the right
if (newRight > maxWidth) {
newRight = maxWidth;
newLeft = newRight - elmnt.offsetWidth;
}
// Prevent dragging off the bottom
if (newBottom > maxHeight) {
newBottom = maxHeight;
newTop = maxHeight - elmnt.offsetHeight;
}
// Update element's position
elmnt.style.left = newLeft + "px";
elmnt.style.top = newTop + "px";
// Update last mouse positions
pos3 = e.clientX;
pos4 = e.clientY;
console.log("dragging",newRight,maxWidth);
}
function getLastDigit(str) {
const match = str.match(/\d$/); // Match a digit at the end of the string
return match ? match[0] : null; // Return the digit if found, otherwise null
}
function closeDragElement(e) {
e.preventDefault();
e.stopPropagation();
if(elmnt.id.startsWith("draggableTitle"))
{ /* param is "[index,x1,y1,x2,y2]" specifying which graph,
and the pixel coordinates of the title window, relative
to the rectangle that contains
the graphs but not the graph toolbar. */
let x1 = parseFloat(elmnt.style.left);
let y1 = parseFloat(elmnt.style.top);
let x2 = x1 + parseFloat(elmnt.style.width);
let y2 = y1 + parseFloat(elmnt.style.height);
index = getLastDigit(elmnt.id);
param = `[${index},${x1},${y1},${x2},${y2}]`;
sendMessageToEngine("graphTitleMoved", param);
// no need to await the result, so no need to label this function async
}
document.onpointerup = null;
document.onpointermove = null;
// Delay the reassignment by 50 ms to prevent unwanted clicks
setTimeout(() => {
mouseFunction = oldmouseFunction;
}, 50);
}
}
// End of title-dragging code.
// implement the Toolbar buttons that don't require that the Engine resend the document:
function turnOnMouseScroll(){
mouseFunction = mouseScrollGraph;
console.log("mouseFunction is", mouseFunction);
if (svgContainer.contains(selectionRectangle))
svgContainer.removeChild(selectionRectangle);
}
function turnOnSelectRectangle(){
mouseFunction = mouseSelectRectangle;
console.log("mouseFunction is", mouseFunction);
}
function turnOnSelectCenteredRectangle(){
mouseFunction = mouseSelectCenteredRectangle;
console.log("mouseFunction is", mouseFunction);
}
function turnOnPointSlope(){
mouseFunction = mousePointSlope;
// pointSlopeOn = true; Not yet, only on mousedown
console.log("mouseFunction is", mouseFunction);
}
function showAssumptions() {
// Create a popup window
var popupWindow = window.open('', 'Assumptions', 'width=600, height=400');
// Create a div with relative positioning
var div = popupWindow.document.createElement('div');
div.style.position = 'relative';
// Append the div to the popup window
popupWindow.document.body.appendChild(div);
// Display all SVG elements with class "assumption"
var assumptions = document.querySelectorAll('.assumption');
assumptions.forEach(function (assumption) {
// Clone the assumption
var clonedAssumption = assumption.cloneNode(true);
// Set the display property to block (or any other appropriate value)
clonedAssumption.style.display = 'block';
// Append the cloned assumption to the div
div.appendChild(clonedAssumption);
});
}
function showSingularities() {
// Create a popup window
var popupWindow = window.open('', 'Singularities', 'width=600, height=400');
// Create a div with relative positioning
var div = popupWindow.document.createElement('div');
div.style.position = 'relative';
// Append the div to the popup window
popupWindow.document.body.appendChild(div);
// Display all SVG elements with class "singularity"
var singularities = document.querySelectorAll('.singularity');
singularities.forEach(function (t) {
var clonedSingularity = t.cloneNode(true);
// Set the display property to block (or any other appropriate value)
clonedSingularity.style.display = 'block';
// Append the cloned assumption to the div
div.appendChild(clonedSingularity);
});
}
function showJumps() {
// Create a popup window
var popupWindow = window.open('', 'Singularities', 'width=600, height=400');
// Create a div with relative positioning
var div = popupWindow.document.createElement('div');
div.style.position = 'relative';
// Append the div to the popup window
popupWindow.document.body.appendChild(div);
// Display all SVG elements with class "jump"
var jumps = document.querySelectorAll('.jump');
jumps.forEach(function (t) {
var clonedJump = t.cloneNode(true);
// Set the display property to block (or any other appropriate value)
clonedJump.style.display = 'block';
// Append the cloned assumption to the div
div.appendChild(clonedJump);
});
}
function svgToImageInput(svgElementId, inputElementId) {
var svgElement = document.getElementById(svgElementId);
var inputElement = document.getElementById(inputElementId);
// Serialize SVG to string
var serializer = new XMLSerializer();
var svgString = serializer.serializeToString(svgElement);
// Encode SVG string in Base64
var svgBase64 = 'data:image/svg+xml;base64,' + window.btoa(unescape(encodeURIComponent(svgString)));
// Set the encoded SVG as the source for the input image
inputElement.setAttribute('src', svgBase64);
}
// Ensure this script runs after the DOM is fully loaded
document.addEventListener('DOMContentLoaded', function() {
// <svg> elements come from the Engine with top and left set, but SVG ignores those.
// We must set them as CSS attributes.
let graphs = document.querySelectorAll('.graph');
for (let graph of graphs) {
let top = graph.getAttribute("top");
let left = graph.getAttribute("left");
if (top !== null) {
graph.style.top = `${top}px`;
}
if (left !== null) {
graph.style.left = `${left}px`;
}
}
// now for the titles
let title0 = document.getElementById('title0');
if (title0) {
let left = title0.style.left;
let top = title0.style.top;
title0.style.display = "block";
// now (left,top) is where we want draggableTitle0 to be
let draggableTitle0 = document.getElementById("draggableTitle0");
if (draggableTitle0)
{
draggableTitle0.style.left = left;
draggableTitle0.style.top = top;
draggableTitle0.style.display = "block";
moveSvgElementToContainer('title0', 'draggableTitle0');
// that changes title0.left and title0.top to 0
// moveSvgElementToContainer('titleRect0', 'draggableTitle0');
// titleRect0 is no longer sent by the Engine; interface controls title background
}
}
// Now do the same for title1 through title5 if they exist
for (let k = 1; k < 6; k++) {
// 6 is MAXGRAPHS in the Engine
let id = "title" + k;
let title = document.getElementById(id);
if (title === null)
{ // console.log("breaking because title doesn't exist");
break;
// This works with === but not with ==, which I think is a Javascript bug
}
let titleRectId = "titleRect" + k;
let titleRect = document.getElementById(titleRectId);
let draggableTitleId = "draggableTitle" + k;
let draggableTitle = document.getElementById(draggableTitleId);
let left = title.style.left;
let top = title.style.top;
draggableTitle.style.left = left;
draggableTitle.style.top = top;
draggableTitle.style.display = "block"; // it was none before
title.style.display = "block";
if (draggableTitle) {
moveSvgElementToContainer(id, draggableTitleId);
// moveSvgElementToContainer(titleRectId, draggableTitleId);
// obsolete, interface now controls title background
}
}
});
let selectionMade = false; // set it to true on mouse_up when a rectangle is selected
document.addEventListener('DOMContentLoaded', function() {
showCrashPopup(); // if a crash has occurred, notify the user.
// Give the Draw button something to do when it's clicked:
document.getElementById('draw').addEventListener('click', function(event) {
// Construct the selected rectangle string
// The form will submit automatically as this button is of type image/input inside the form. Therefore:
event.preventDefault(); // Prevent the default form submission
// console.log("Draw button was clicked");
// console.log("Value of selectionMade = ");
// console.log(selectionMade);
if (selectionMade) { // Check if a selection has been made
let graphwidth = <?php echo($_SESSION['graphWidth']);?>;
let windowheight = <?php echo($_SESSION['height']);?>;
let toolbarwidth = <?php echo($toolbarwidth);?>;
let windowwidth = graphwidth+toolbarwidth;
let selectedRectangleValue = "[" + [whichgraph, parseInt(selectedX), parseInt(selectedY), parseInt(selectedWidth), parseInt(selectedHeight),parseInt(toolbarwidth), windowwidth, windowheight].join(',') + "]";
// coordinate parameters are expected by the Engine to be integers either all in pixels or all without units. Here, all are withouit units, as shown by console.log.
// console.log(selectedRectangleValue)
// return; // use this to see the console.log output
document.getElementById('selectedRectangleField').value = selectedRectangleValue;
document.getElementById('mouseFunctionField').value = mouseFunction;
// Now submit it
selectionMade = false; // we're done with it
document.getElementById('myForm').submit();
} // end if
} // end of the function
); // end of addEventListener
// Create and label buttons to increment and decrement parameters
// Step 1: Identify all SVG elements of class "param"
let firstsvg = true; // while true, processing the first element of class param
const svgs = document.querySelectorAll('svg.param');
// Step 2 & 3: Process each SVG and create corresponding button groups
svgs.forEach((svg, index) => {
/* // this code saved in case it's someday useful;
// but not used here because can't color an image in CSS
// Convert SVG to Data URL
const xml = new XMLSerializer().serializeToString(svg);
const svg64 = btoa(unescape(encodeURIComponent(xml)));
const svgDataUrl = 'data:image/svg+xml;base64,' + svg64;
svg.style.display = "none"; // Hide the original SVG to prevent clutter.
// svg.parentNode.removeChild(svg);
const inputImage = document.createElement('input');
inputImage.type = 'image';
inputImage.src = svgDataUrl;
inputImage.alt = `Parameter ${index}`;
inputImage.style.width = '24px';
inputImage.style.height = '24px';
*/
// Create button group container with specified styles
const buttonGroup = document.createElement('div');
buttonGroup.className = 'button-group';
buttonGroup.style.display = 'flex';
buttonGroup.style.flexDirection = 'row';
buttonGroup.style.alignItems = 'center';
// Create the main button with SVG in it and specified styles
const button = document.createElement('button');
button.type = 'button'; // Use 'button' type for generic buttons
button.alt = `Parameter ${index}`;
button.id = `parameterButton${index}`;
button.style.width = '24px';
button.style.height = '24px';
button.style.backgroundColor = 'rgb(0,0,128)';
button.style.color = 'white';
button.style.border = 'none';
// button.style.position = 'absolute'; // see change 4749 in record4.txt
// Add a class so we can do something to all these buttons if we want
button.classList.add('buttonsvg');
if (!parameters) {
parameters = jsonParameterData.parameters || [];
}
button.addEventListener('pointerdown', (event) => {
if (lastPointerId !== null) {
svgContainer.releasePointerCapture(lastPointerId);
console.log("releasing lastPointerId");
mouseFunction = null;
}
lastPointerId = event.pointerId; // Store pointer ID for releasing later
// Get the click coordinates from the PointerEvent
const clickX = event.clientX;
const clickY = event.clientY;
adjustParameter(clickX, clickY-100,index,svgContainer); // see AdjustParameter.js
// This just sets up event listeners to the sliders in the parameter dialog, nothing to do with the parameter sliders in
// the Graph Toolbar.
// When the Done button is pressed, it's handled in AdjustParameter.js
});
// change the color of the svg
svg.querySelectorAll('text').forEach(textElement => {
textElement.style.fill = 'white'; // Set the text color to white
});
svg.setAttribute('height', '24');
// Append the SVG to the button
button.appendChild(svg);
// Append the button to the desired parent element
buttonGroup.appendChild(button);
// add an event listener so Tooltips works
button.addEventListener('mouseenter', showTooltip);
button.addEventListener('mouseleave', hideTooltip);
// Container for the + and - buttons, aligned vertically
const buttonsContainer = document.createElement('div');
buttonsContainer.style.display = 'flex';
buttonsContainer.style.flexDirection = 'column';
buttonsContainer.style.alignItems = 'center';
// Create the "+" button with specified styles for background and text color
const inputPlus = document.createElement('button');
inputPlus.innerHTML = "+";
inputPlus.style.cssText = 'height: 12px; width: 12px; background-color: rgb(0,0,128); color: white; border: none; cursor: pointer;';
inputPlus.id = `parameterIncrementButton${index}`;
// add an event listener so Tooltips works
inputPlus.addEventListener('mouseenter', showTooltip);
inputPlus.addEventListener('mouseleave', hideTooltip);
// Create the "-" button with specified styles for background and text color
const inputMinus = document.createElement('button');
inputMinus.innerHTML = "-";
inputMinus.style.cssText = 'height: 12px; width: 12px; background-color: rgb(0,0,128); color: white; border: none; cursor: pointer;';
inputMinus.id = `parameterDecrementButton${index}`;
// add an event listener so Tooltips works
inputMinus.addEventListener('mouseenter', showTooltip);
inputMinus.addEventListener('mouseleave', hideTooltip);
// Event listeners for "+" and "-" buttons to submit the form
inputPlus.addEventListener('click', function() {
submitFormWithAction('parameterplus' + index);
});
inputMinus.addEventListener('click', function() {
submitFormWithAction('parameterminus' + index);
});
// Append + and - buttons to their container
buttonsContainer.appendChild(inputPlus);
buttonsContainer.appendChild(inputMinus);
// Append elements to the button group
buttonGroup.appendChild(buttonsContainer); // Container with + and - buttons
if(firstsvg &&
graphtypeNumber != 8 && // don't put the toggleEraseButton on POLYROOT or MC_INEQ or MC_SET
graphtypeNumber != 3 && // these are values from mainchoi.h
graphtypeNumber != 4 // the Engine sends Javascript to define graphtypeNumber
)
{ const toggleEraseButton = document.createElement('button');
let redcircle = "🔴";
let greencircle = "🟢";
if (eraseOld)
toggleEraseButton.innerHTML = greencircle; // thus this is the default, if it wasn't already set.
else
toggleEraseButton.innerHTML = redcircle;
toggleEraseButton.style.cssText = 'height: 24px; width: 25px; background-color: rgb(0,0,128); color: white; border: none; cursor: pointer;';
toggleEraseButton.id = "toggleErase";
toggleEraseButton.addEventListener('click',function(){
submitFormWithAction('toggleErase');
});
// add an event listener so Tooltips works
toggleEraseButton.addEventListener('mouseenter', showTooltip);
toggleEraseButton.addEventListener('mouseleave', hideTooltip);
buttonGroup.appendChild(toggleEraseButton);
firstsvg = false; // only create one such button!
}
// Append the button group to the target container
document.getElementById('toolbar').appendChild(buttonGroup);
let parameterName = parameters[index].name;
if(supportsSlider(graphtypeNumber,parameterName)){ // supportsSlider is in ParameterSlider.js
// Make a slider to control this parameter. See ParameterSlider.js
makeParameterSlider(index, parameters, document.getElementById("toolbar"));
}
});
// Create and label hidden buttons to increment and decrement the parameter
const inputPlusActive = document.createElement('button');
inputPlusActive.style.cssText = 'height: 0px; width: 0px; display:none';
inputPlusActive.id = "incrementActiveParameterButton";
inputPlusActive.addEventListener('click', function() {
submitFormWithAction('incrementActiveParameter');
});
const inputMinusActive = document.createElement('button');
inputMinusActive.style.cssText = 'height: 0px; width: 0px; display:none';
inputMinusActive.id = "decrementActiveParameterButton";
inputMinusActive.addEventListener('click', function() {
submitFormWithAction('decrementActiveParameter');
});
document.getElementById('toolbar').appendChild(inputPlusActive);
document.getElementById('toolbar').appendChild(inputMinusActive);
});
function submitFormWithAction(actionValue) {
const form = document.getElementById('myForm');
const actionInput = document.getElementById('formAction');
actionInput.value = actionValue;
form.submit();
}
let svgContainer = document.getElementById('svgContainer');
let selectionRectangle = document.createElement('div');
selectionRectangle.id = 'selectionRectangle';
selectionRectangle.style.position = 'absolute';
selectionRectangle.style.border = '1px solid blue';
selectionRectangle.style.pointerEvents = 'none'; // Ignore pointer events
// svgContainer.appendChild(selectionRectangle);
// This is now done on pointerdown, when selection starts. If it's done here,
// it makes a visible black dot below the graph and creates scroll bars.
let selectedX = 0;
selectionRectangle.style.left = 0+"px";
let selectedY = 0;
selectionRectangle.style.top = 0+"px";
let selectedWidth = parseInt(selectionRectangle.style.width,10);
let selectedHeight = parseInt(selectionRectangle.style.height,10);
let startX = 0;
let startY = 0;
let initialX = 0;
let initialY = 0;
let isScrolling = false;
let isSelecting = false;
let activeGraphBounds = null;
let whichgraph = 0;
const svg0 = document.getElementById('graph0');
// the svgContainer will receive mouse events, and
// will handle the selected rectangle and also the scrolling of graphs.
// The scrollable elements are the <g> groups that contain each
// graph; but if two graphs (or more) are drawn on the same axes, they both (or all)
// must scroll, which I could only arrange by having svgContainer process
// the mouse messages
let firstGraphIndex = -1
async function handleClick(event) {
// Get the click position within svgContainer
// This is used when graphing ODEs with a direction field
console.log("In handleClick");
// Arrange that ODEhint should not be shown again
// we want to set $_SESSION['ODEhint'], but that has to be done in PHP, not here in Javascript
fetch("ODEhint.php", { method: "POST" }); // ODEhint.php will set $_SESSION['ODEhint']
if(mouseFunction > 0)
{ console.log("returning, because mouseFunction = ", mouseFunction);
return; // we are dragging, or selecting a rectangle
}
const svgContainer = document.getElementById("svgContainer");
const rect = svgContainer.getBoundingClientRect();
// Calculate x and y coordinates relative to svgContainer
const clickX = event.clientX - rect.left;
const clickY = event.clientY - rect.top;
if (typeof sendMessageToEngine === 'undefined') {
console.error("sendMessageToEngine is not defined!");
return;
}
// Send the message to the engine
try{
const response = await sendMessageToEngine("askPointSlope", `[${clickX},${clickY}]`);
console.log(response);
// response has the form [x,y] as plain text, not SVG
// so the following line extracts x and y
const [x, y] = response.slice(1, -1).split(",");
// Now find the parameters that corresponds to x0 and y0, adjust them, and
// arrange for an updateParameters message to be sent.
// They are always the last two parameters
// Make sure the parameter data has been upacked:
if (!parameters) {
parameters = jsonParameterData.parameters || [];
console.log("oops! Shouldn't happen")
}
if(parameters.length < 2){
console.log("oops! less than 2 parameters in an ODE.");
return;
}
console.log("got here");
let nparameters = parameters.length;
parameters[nparameters-2].value = x;
parameters[nparameters-1].value = y;
updateParameters(); // defined in AdjustParameters.js.
// It submits a form, which when processed sends an updateParameters message to the Engine.
}
catch (error) {
console.error('Error in handleClick:', error);
return;
}
}
if(graphtypeNumber == ODE || graphtypeNumber == ODE2){
// graphtypeNunmber is defined in code emitted by the Engine by sendJavascript
mouseFunction = 0;
// if a rectangle is selected and Draw is pressed, mousefunction will be saved in SESSION so use can continue selecting rectangles;
// but in ODE2 that's not good, as it disables starting a new solution by clicking. So this should prevent it. In ODE2, if you want
// to select another rectangle, you'll have to click one of the select buttons first.
svgContainer.addEventListener('click', handleClick);
// this is used to let a click start a new solution line on the graph
const ODEhint = document.getElementById("ODEhint");
console.log("show_ODEhint = ", show_ODEhint);
if(!show_ODEhint)
{ ODEhint.style.opacity = 0; // Make it invisible
console.log("making ODEhint invisible");
}
else
{ ODEhint.style.opacity = 1; // Make it visible
console.log("making ODEhint visible");
}
}
let lastPointerId = null; // Global variable to store the pointer ID
svgContainer.addEventListener('pointerdown', function(e) {
if(mouseFunction == mousePointSlope || mouseFunction == mouseSelectRectangle ||
mouseFunction == mouseSelectCenteredRectangle || mouseFunction == mouseScrollGraph
)
{ e.preventDefault();
svgContainer.setPointerCapture(e.pointerId);
// if these lines aren't inside the above "if" then they disable the AdjustParameters sliders
lastPointerId = e.pointerId; // Store pointer ID for releasing later
console.log("setting lastPointerId to ", e.pointerId);
}
selectionMade = false;
if (mouseFunction == mousePointSlope)
{
pointSlopeOn = true;
handleMouseMove(e);
return;
}
if (!svgContainer.contains(selectionRectangle))
svgContainer.appendChild(selectionRectangle);
let graphs = document.querySelectorAll('.graph');
let rect = svgContainer.getBoundingClientRect();
let x = e.clientX - rect.left;
let y = e.clientY - rect.top;
let k = 0;
for (let graph of graphs) {
// Use the manually defined bounds instead of getBoundingClientRect()
let graphX = parseInt(graph.getAttribute("left"), 10);
let graphY = parseInt(graph.getAttribute("top"), 10);
let graphWidth = parseInt(graph.getAttribute("mywidth"), 10);
let graphHeight = parseInt(graph.getAttribute("myheight"), 10);
// console.log("in mousedown: ", graphX, graphY, graphWidth, graphHeight,mouseFunction,x,y);
if (x >= graphX && x <= graphX + graphWidth && y >= graphY && y <= graphY + graphHeight)
{ startX = x;
startY = y;
if(firstGraphIndex == -1)
firstGraphIndex = k; // this will be used on mouseup to know which graph to scroll
if(mouseFunction == mouseSelectRectangle || mouseFunction == mouseSelectCenteredRectangle)
{ isSelecting = true;
// console.log("set isSelecting to true because svgContainer got mousedown");
activeGraphBounds = { left: graphX, top: graphY, right: graphX + graphWidth, bottom: graphY + graphHeight };
updateSelectionRectangle(x, y, 0, 0);
break;
}
else if(mouseFunction == mouseScrollGraph)
{
isScrolling = true;
const transform = `translate(0,0)`;
elmnt = graph.querySelector('g.scrollable'); // the scrollable <g> of the graph
elmnt.setAttribute('transform', transform);
isSelecting = false; // prevent selection rectangle
// console.log("set isSelecting to false");
// do not break out of the loop.
// do this to all the graphs containing the mouse point,
// not just the first one.
}
}
k=k+1;
}
});
let graphs = document.querySelectorAll('.graph');
function handleMouseMove(e){
if ((mouseFunction == mouseSelectRectangle || mouseFunction == mouseSelectCenteredRectangle) && ( !isSelecting || !activeGraphBounds) )
return;
if(mouseFunction == mouseScrollGraph && !isScrolling)
return;
let rect = svgContainer.getBoundingClientRect();
let x = e.clientX - rect.left;
let y = e.clientY - rect.top;
if (isSelecting && (mouseFunction == mouseSelectRectangle || mouseFunction == mouseSelectCenteredRectangle))
{
// Constrain the selection or the scroll within the manually defined bounds
x = Math.max(activeGraphBounds.left, Math.min(x, activeGraphBounds.right));
y = Math.max(activeGraphBounds.top, Math.min(y, activeGraphBounds.bottom));
let deltax = x - startX;
let deltay = y - startY;
let width = Math.abs(deltax);
let height = Math.abs(deltay);
if(mouseFunction == mouseSelectCenteredRectangle)
{
width *= 2;
height *= 2;
}
let newX = startX < x ? startX : x;
let newY = startY < y ? startY : y;
// Adjust width and height to not exceed the active graph bounds
if (newX + width > activeGraphBounds.right)
width = activeGraphBounds.right - newX;
if (newY + height > activeGraphBounds.bottom)
height = activeGraphBounds.bottom - newY;
if (mouseFunction != mouseScrollGraph)
updateSelectionRectangle(newX, newY, width, height);
}
else if(mouseFunction == mouseScrollGraph && isScrolling)
{
// scroll all the graphs that contain (e.clientX, e.clientY)
for (let graph of graphs) {
// Use the manually defined bounds instead of getBoundingClientRect()
let graphX = parseInt(graph.getAttribute("left"), 10);
let graphY = parseInt(graph.getAttribute("top"), 10);
let graphWidth = parseInt(graph.getAttribute("mywidth"), 10);
let graphHeight = parseInt(graph.getAttribute("myheight"), 10);
console.log(graphX, graphY, graphWidth, graphHeight);
if (x >= graphX && x <= graphX + graphWidth && y >= graphY && y <= graphY + graphHeight)
{
var dx = e.clientX -rect.left - startX;
var dy = e.clientY -rect.top - startY;
const transform = `translate(${dx}, ${dy})`;
elmnt = graph.querySelector('g.scrollable'); // the scrollable <g> of the graph
elmnt.setAttribute('transform', transform);
}
}
}
else if(mouseFunction == mousePointSlope && pointSlopeOn)
{ // Initialize the parameters and data string
var param = `[${x},${y}]`;
var data = `askPointSlopeParam=${encodeURIComponent(param)}`;
// Create the XMLHttpRequest object and set up the request
var xhr = new XMLHttpRequest();
xhr.open("POST", "<?php echo ($_SERVER['PHP_SELF']); ?>", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
// Set up an event listener to handle the response
xhr.onreadystatechange = function() {
if (xhr.readyState === 4)
{
if (xhr.status === 200)
{
pointSlopeResponse = xhr.responseText;
console.log('Response received:', pointSlopeResponse);
// pointSlopeResponse consists of two parts: [px,py] followed by PointSlopeSVG
const marker = pointSlopeResponse.indexOf(']') + 1; // start of the SVG
let coordinates = pointSlopeResponse.substring(0,marker);
pointSlopeSVG = pointSlopeResponse.substring(marker);
// Now get this SVG displayed in the PointSlopeElementDiv
pointSlopeElement = document.getElementById("PointSlopeDiv");
pointSlopeElement.style.display = "block";
pointSlopeElement.style.backgroundColor = "lightblue";
pointSlopeElement.style.borderColor = "darkblue";
pointSlopeElement.innerHTML = pointSlopeSVG;
// Wait for the SVG to be inserted into the DOM and set width and height
requestAnimationFrame(() => {
const svgElement = pointSlopeElement.querySelector('svg');
if (svgElement) {
const bbox = svgElement.getBBox();
let newwidth = bbox.width + 10;
let newheight = bbox.height; // doesn't look good larger
pointSlopeElement.style.width = `${newwidth}px`;
pointSlopeElement.style.height = `${newheight}px`;
}
});
// Now extract the coordinates px,py as numbers
console.log(coordinates);
const [px,py] = coordinates.slice(1, -1).split(',').map(Number);
// Now move the crosshairs there
const container = document.getElementById('crosshairs-container');
container.style.left = `${px - 19}px`;
container.style.top = `${py - 19}px`;
container.style.display = "block"; // make it visible
console.log(container);
}
else
{
console.error('Error with request, status code:', xhr.status);
console.error('Response received:', xhr.responseText);
}
}
}
// Send the AJAX message notifying the Engine
console.log('Data being sent:', data);
xhr.send(data);
}
}
svgContainer.addEventListener('pointermove', handleMouseMove);
svgContainer.addEventListener('pointerup', function(e) {
svgContainer.releasePointerCapture(e.pointerId);
if(mouseFunction == mousePointSlope)
{
pointSlopeOn = false;
}
if(mouseFunction == mouseScrollGraph)
{
isScrolling = false;
// get the element to scroll
elmnt = document.getElementById("scrollable"+firstGraphIndex);
// How much did it scroll?
let rect = svgContainer.getBoundingClientRect();
let x = e.clientX - rect.left;
let y = e.clientY - rect.top;
let deltax = x - startX;
let deltay = y - startY;
if(deltax == 0 && deltay == 0)
return; // don't bother telling the Engine nothing happened
// Next we will submit a form that, when processed, will
// send a graphMoved message to the Engine
// Form the parameter in the required format
let graphwidth = <?php echo($_SESSION['graphWidth']);?>;
let height = <?php echo($_SESSION['height']);?>;
var param = `[${firstGraphIndex},${deltax},${deltay},${graphwidth},${height}]`;
// submit a hidden form so param will become $_POST['graphMovedParam']
let form = document.createElement('form');
form.method = 'POST';
form.action = "<?php echo ($_SERVER['PHP_SELF']); ?>";
// Create a hidden input to hold the param value
let input = document.createElement('input');
input.type = 'hidden';
input.name = 'graphMovedParam';
input.value = param;
// Append the input to the form
form.appendChild(input);
// Append the form to the body
document.body.appendChild(form);
// Submit the form
form.submit();
}
if (!isSelecting)
return;
// So now we have selected a rectangle (unless its width or height is zero)
isSelecting = false;
activeGraphBounds = null; // Reset the active graph bounds
// The final selected rectangle is available here
selectedX = selectionRectangle.style.left;
selectedY = selectionRectangle.style.top;
selectedWidth = selectionRectangle.style.width;
selectedHeight = selectionRectangle.style.height;
if(parseInt(selectedWidth) == 0 || parseInt(selectedHeight) == 0)
return; // you can't select a rectangle with zero width or height
selectionMade = true;
// console.log("on mouse up, whichgraph and selected rectangle:");
// console.log(whichgraph,selectedX,selectedY,selectedWidth,selectedHeight);
isScrolling = false;
// When the Draw button is clicked, the selected rectangle
// will be submitted in a form, and when that form is processed,
// a selectedRectangleGraph message will be sent to the Engine
// with the rectangle as parameter.
});
function updateSelectionRectangle(x, y, width, height) {
selectionRectangle.style.left = x + 'px';
selectionRectangle.style.top = y + 'px';
selectionRectangle.style.width = width + 'px';
selectionRectangle.style.height = height + 'px';
}
<?php include "wheel.php" ?> // for dragging on a touchpad with two fingers
<?php include "touch.php" ?> // for dragging on iPad or iPhone with two fingers.
/*______________________________________________________________*/
/* script for resizing the window */
/* We don't want to contact the server until the resize operation
is completed. But that is not an event. So instead, we just wait
300 milliseconds after a resize event to contact the server
*/
let resizeTimer;
function onResizeEnd()
{ console.log("in onResizeEnd");
const newWidth = window.innerWidth;
const newHeight = parseInt(window.innerHeight);
toolbarwidth = <?php echo($toolbarwidth); ?>;
toolbarwidth = toolbarwidth;
graphwidth = newWidth - toolbarwidth;
if(graphwidth <= 0)
return; // just ignore this event
graphwidth = parseInt(graphwidth); // Engine expects integer
let param = `[${graphwidth},${newHeight}]`;
let form = document.createElement('form');
form.method = 'POST';
form.action = "<?php echo ($_SERVER['PHP_SELF']); ?>";
// Create a hidden input to hold the param value
let input = document.createElement('input');
input.type = 'hidden';
input.name = 'graphWindowResizedField';
input.value = param;
// Append the input to the form
form.appendChild(input);
// we also need the new width and height
// to be stored in $_SESSION variables so they will
// be available to set the size of svgContainer after Engine
// returns an answer to the graphWindowResized message
// To get them back to PHP they have to be attachd to this form
// Create hidden inputs to hold the new width and height
let input1 = document.createElement('input');
let input2 = document.createElement('input');
input1.type = 'hidden';
input1.name = 'graphwidth';
input1.value = graphwidth;
form.appendChild(input1);
input2.type = 'hidden';
input2.name = 'newHeight';
input2.value = newHeight;
form.appendChild(input2);
// Append the form to the body
document.body.appendChild(form);
// Submit the form
form.submit();
}
window.addEventListener('resize', () =>
{
// Clear the previous timer
clearTimeout(resizeTimer);
// Set a new timer
resizeTimer = setTimeout(() =>
{
console.log('Resize ended');
onResizeEnd();
}, 300); // Adjust the timeout value as needed
});
</script>
<script>
// code to control slider knob colors
document.addEventListener("DOMContentLoaded", function () {
console.log("DOM fully loaded. Active parameter:", activeParameter);
<?php if ($resetSliderColors): ?>
console.log("Form submitted: Resetting all sliders to white.");
document.querySelectorAll("input[type=range]").forEach(slider => {
slider.classList.remove("active-slider"); // Reset all to white
});
<?php else: ?>
console.log("Normal page load: Setting active slider to green.");
updateSliderColors(); // ✅ Ensures active slider is green on load
<?php endif; ?>
// ✅ Listen for ALL relevant events
document.querySelectorAll("input[type=range]").forEach(slider => {
slider.addEventListener("pointerup", updateSliderColors); // Pointer already down
slider.addEventListener("click", updateSliderColors); // Edge cases
});
});
function updateSliderColors() {
document.querySelectorAll("input[type=range]").forEach(slider => {
let sliderId = slider.id.match(/\d+$/);
if (sliderId && parseInt(sliderId[0]) === activeParameter) {
slider.classList.add("active-slider"); // ✅ Turn green immediately
} else {
slider.classList.remove("active-slider"); // ❌ Keep others white
}
});
}
</script>
</body>
</html>
Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists