csfieldguide/static/interactives/matrix-simplifier/js/matrix-simplifier.js
const dragula = require('dragula');
const mathjs_import = require('mathjs');
const sprintf = require('sprintf-js').sprintf;
const vsprintf = require('sprintf-js').vsprintf;
const ROW_TEMPLATE = "%s & %s & %s";
const MATRIX_TEMPLATE = "\\begin{bmatrix} %s \\\\ %s \\\\ %s \\end{bmatrix}";
const PENCIL_SVG = $("#pencil-svg-helper svg");
const DRAGGER_SVG = $("#dragger-svg-helper svg");
var TXT_COPY = gettext("Copy to clipboard");
var TXT_COPIED_SUCCESS = gettext("Equation copied");
var TXT_COPIED_FAIL = gettext("Oops, unable to copy. Please copy manually");
const MATRIX_CHILD_INDEX = 3; // Index for which child of the draggable element/div is the actual matrix
/**
* Below is adapted from https://mathjs.org/examples/browser/angle_configuration.html.html
* This is used to configure mathjs to accept degrees as input for trig functions.
*/
const mathjs = mathjs_import.create(mathjs_import.all);
let replacements = {};
// the trigonometric functions that we are configuring to handle inputs of degrees instead of radians
const fns1 = [
'sin', 'cos', 'tan', 'sec', 'cot', 'csc',
'asin', 'acos', 'atan', 'atan2', 'acot', 'acsc', 'asec',
'sinh', 'cosh', 'tanh', 'sech', 'coth', 'csch',
'asinh', 'acosh', 'atanh', 'acoth', 'acsch', 'asech',
];
fns1.forEach(function(name) {
const fn = mathjs[name]; // the original function
const fnNumber = function (x) {
// convert from degrees to radians
return fn(x * (Math.PI / 180));
}
// create a typed-function which check the input types
replacements[name] = mathjs.typed(name, {
'number': fnNumber,
'Array | Matrix': function (x) {
return mathjs.map(x, fnNumber);
}
});
});
// import all replacements into math.js, override existing trigonometric functions
mathjs.import(replacements, {override: true});
/////////////////////////////// End of adapted file ///////////////////////////////
// matrices and vector that will appear onload
m1 = mathjs.matrix([
[mathjs.cos(45),0,mathjs.sin(45)],
[0,1,0],
[-mathjs.sin(45),0,mathjs.cos(45)]
]);
m2 = mathjs.matrix([[10,0,0],[0,10,0],[0,0,10]]);
v1 = mathjs.matrix([[10], [0], [0]]);
// Arrays that will keep track of the order the matrices and vectors are in
var currentMatricesOrder = [m1, m2];
var currentVectorsOrder = [v1];
m1String = [
['cos(45)', '0', 'sin(45)'],
['0', '1', '0'],
['-sin(45)', '0', 'cos(45)']
];
m2String = [
['10', '0', '0'],
['0', '10', '0'],
['0', '0', '10']
];
v1String = [
['10'],
['0'],
['0']
];
// Arrays that will hold the string version of matrices and vectors.
// Used for populating modal when editing a matrix or vector.
// Needed because we don't want to display evaluated trig functions.
// E.g we want to show cos(45) and not 0.71
var matricesStringFormat = [m1String, m2String];
var vectorsStringFormat = [v1String];
// Store the result equation. Used for copying to clipboard.
var resultEqtn;
// only show equations once they are rendered
// URL for mathjax script loaded from CDN
var mjaxURL = 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.9/MathJax.js?config=TeX-AMS-MML_HTMLorMML,Safe.js';
// load mathjax script
$.getScript(mjaxURL, function() {
// mathjax successfully loaded, let it render
showOutput();
});
$(document).ready(function() {
$('#add-matrix-from-input').click(addMatrix);
$('#add-vector-from-input').click(addVector);
$('.dismiss-eqtn').on("click", dismissEquation);
$('.edit-eqtn').on("click", populateModalForEditing);
$('.matrix-row input').on('keyup bind cut copy paste', validateInput);
$('#matrix-modal').on('hidden.bs.modal', resetModalMatrices);
$('#vector-modal').on('hidden.bs.modal', resetModalMatrices);
$("#remove-all-matrices").on("click", removeAllMatrices);
$("#remove-all-vectors").on("click", removeAllVectors);
$('#copy-eqtn').click(function() {
$('#code-to-copy').select();
try {
var successful = document.execCommand('copy');
if (successful) {
$('#copy-eqtn').trigger('copied', TXT_COPIED_SUCCESS);
} else {
$('#copy-eqtn').trigger('copied', TXT_COPIED_FAIL);
}
} catch (err) {
$('#copy-eqtn').trigger('copied', TXT_COPIED_FAIL);
}
});
$('[data-toggle="tooltip"]').on('copied', function(event, message) {
$(this).attr('title', message)
.tooltip('_fixTitle')
.tooltip('show')
.attr('title', TXT_COPY)
.tooltip('_fixTitle');
});
});
/**
* Add a new matrix to the calculation
*/
function addMatrix() {
// inputs get evaluated as math
matrixArrayMath = getMatrix(true);
// inputs remain as strings for display
matrixArrayString = getMatrix(false);
matricesStringFormat.push(matrixArrayString);
matrix = mathjs.matrix(matrixArrayMath); // convert to mathjs matrix so we can do calculations
currentMatricesOrder.push(matrix);
matrixString = formatMatrix(matrixArrayString, ROW_TEMPLATE);
appendInput('matrix', matrixString);
showOutput();
}
/**
* Add a new vector to the calculation
*/
function addVector() {
// inputs get evaluated as math
vectorArrayMath = [
[mathjs.evaluate($('#vector-row-0').val())],
[mathjs.evaluate($('#vector-row-1').val())],
[mathjs.evaluate($('#vector-row-2').val())]
];
// inputs remain as strings for display
vectorArrayString = [
[$('#vector-row-0').val()],
[$('#vector-row-1').val()],
[$('#vector-row-2').val()]
];
vectorsStringFormat.push(vectorArrayString);
vector = mathjs.matrix(vectorArrayMath); // convert to mathjs matrix so we can do caluclations
currentVectorsOrder.push(vector);
vectorString = sprintf(
MATRIX_TEMPLATE,
vectorArrayString[0],
vectorArrayString[1],
vectorArrayString[2]
);
appendInput('vector', vectorString);
showOutput();
}
/**
* Appends either a new matrix or vector to the DOM
*/
function appendInput(type, inputHtml) {
var $newContainerDiv = $("<div>").addClass('draggable content border rounded m-1 center-block');
var $newInputDiv = $("<div>").addClass('invisible ' + type);
var $moveLabel = $('<span class="close move-eqtn" aria-label="Move">');
$moveLabel.append(DRAGGER_SVG.clone());
$moveLabel.append($('<div class="handle cover">')); // The actual handle element
var $closeButton = $('<button type="button" class="close dismiss-eqtn" aria-label="Close"></button>');
$closeButton.append($('<span aria-hidden="true">×</span>'));
var $editButton = $('<button type="button" class="close edit-eqtn" aria-label="Edit"></button>');
$editButton.append(PENCIL_SVG.clone());
$newContainerDiv.append($moveLabel);
$newContainerDiv.append($closeButton);
$newContainerDiv.append($editButton);
$newInputDiv.html(inputHtml);
$newContainerDiv.append($newInputDiv);
$('#' + type + '-input-container').append($newContainerDiv);
// add event handlers for close and edit buttons
$closeButton.on("click", dismissEquation);
$editButton.on("click", populateModalForEditing);
if (type == 'vector') {
var vectorNum = currentVectorsOrder.length - 1;
$closeButton.attr('id', 'close-vector-' + vectorNum);
$editButton.attr('id', 'edit-vector-' + vectorNum);
$editButton.attr('data-toggle', 'modal');
$editButton.attr('data-target', '#vector-modal');
$newInputDiv.attr('id', 'vector-' + vectorNum);
$newInputDiv.attr('data-vector-order', currentVectorsOrder.length - 1);
MathJax.Hub.Queue(["Typeset", MathJax.Hub, 'vector-' + vectorNum]); // typeset the new vector
$('#remove-all-vectors').attr('disabled', false);
} else {
var matrixNum = currentMatricesOrder.length - 1;
$closeButton.attr('id', 'close-matrix-' + matrixNum);
$editButton.attr('id', 'edit-matrix-' + matrixNum);
$editButton.attr('data-toggle', 'modal');
$editButton.attr('data-target', '#matrix-modal');
$newInputDiv.attr('id', 'matrix-' + matrixNum);
$newInputDiv.attr('data-matrix-order', currentMatricesOrder.length - 1);
MathJax.Hub.Queue(["Typeset", MathJax.Hub, 'matrix-' + matrixNum]); // typeset the new matrix
$('#remove-all-matrices').attr('disabled', false);
}
}
/**
* Gets matrix entries from modal and returns a matrix in array form.
* If evalAsMath is true, each entry will be evaluated as a mathematical expression.
* If evalAsMath is false, each entry will remain as a string.
* This is implemented so we can choose whether trig functions appear to the user as 'cos(45)`
* or as their evaluated value, e.g '0.71'.
*/
function getMatrix(evalAsMath) {
if (evalAsMath){
row0 = [
mathjs.evaluate($('#matrix-row-0-col-0').val()),
mathjs.evaluate($('#matrix-row-0-col-1').val()),
mathjs.evaluate($('#matrix-row-0-col-2').val()),
];
row1 = [
mathjs.evaluate($('#matrix-row-1-col-0').val()),
mathjs.evaluate($('#matrix-row-1-col-1').val()),
mathjs.evaluate($('#matrix-row-1-col-2').val()),
];
row2 = [
mathjs.evaluate($('#matrix-row-2-col-0').val()),
mathjs.evaluate($('#matrix-row-2-col-1').val()),
mathjs.evaluate($('#matrix-row-2-col-2').val()),
];
} else {
row0 = [
$('#matrix-row-0-col-0').val(),
$('#matrix-row-0-col-1').val(),
$('#matrix-row-0-col-2').val(),
];
row1 = [
$('#matrix-row-1-col-0').val(),
$('#matrix-row-1-col-1').val(),
$('#matrix-row-1-col-2').val(),
];
row2 = [
$('#matrix-row-2-col-0').val(),
$('#matrix-row-2-col-1').val(),
$('#matrix-row-2-col-2').val(),
];
}
return [row0, row1, row2];
}
/**
* Puts matrix in LaTeX format to be correctly rendered by MathJax
*/
function formatMatrix(matrix, rowTemplate) {
row0 = vsprintf(rowTemplate, matrix[0]);
row1 = vsprintf(rowTemplate, matrix[1]);
row2 = vsprintf(rowTemplate, matrix[2]);
return sprintf(MATRIX_TEMPLATE, row0, row1, row2);
}
/* Set the matrices in modal to the default values */
function resetModalMatrices() {
// reset to default values of modal matrices
$('#matrix-row-0-col-0').val(1);
$('#matrix-row-0-col-1').val(0);
$('#matrix-row-0-col-2').val(0);
$('#matrix-row-1-col-0').val(0);
$('#matrix-row-1-col-1').val(1);
$('#matrix-row-1-col-2').val(0);
$('#matrix-row-2-col-0').val(0);
$('#matrix-row-2-col-1').val(0);
$('#matrix-row-2-col-2').val(1);
$('#vector-row-0').val(0);
$('#vector-row-1').val(0);
$('#vector-row-2').val(0);
// remove red borders on inputs that had errors and enable add button
$('.matrix-row input').removeClass('input-error');
$('.add-from-input').prop('disabled', false);
// make add button the default and hide edit button
$("#add-matrix-from-input").removeClass("d-none");
$("#update-matrix").addClass("d-none");
$("#add-vector-from-input").removeClass("d-none");
$("#update-vector").addClass("d-none");
// change title back to add a matrix/vector
$('#matrix-modal-title').html(gettext("Add a matrix"));
$('#vector-modal-title').html(gettext("Add a vector"));
}
/**
* Calculates result of matrix multiplication and vector addition.
* Returns array containing result matrix and vector.
*/
function calculateOutput() {
var matrixResult = mathjs.identity(3);
var vectorResult = mathjs.zeros(3, 1);
if (currentMatricesOrder.length == 1) {
matrixResult = currentMatricesOrder[0];
} else if (currentMatricesOrder.length > 1) {
// 2 or more matrices
matricesCopy = currentMatricesOrder.slice(); // copy list for multiplying in place
matrixResult = multiplyMatrices(matricesCopy);
}
if (currentVectorsOrder.length == 1) {
vectorResult = currentVectorsOrder[0];
} else if (currentVectorsOrder.length > 1) {
// 2 or more vectors
vectorsCopy = currentVectorsOrder.slice(); // copy list for adding in place
vectorResult = addVectors(vectorsCopy);
}
return [matrixResult, vectorResult];
}
/**
* Multiply matrices together and return result
*/
function multiplyMatrices(m) {
var multiply = true;
while (multiply) {
if (m.length == 2) {
multiply = false;
return mathjs.multiply(m[0], m[1]);
} else {
result = mathjs.multiply(m[0], m[1]);
// remove the first matrix in array
m.shift();
// replace the new first element in array with result
m[0] = result;
}
}
}
/**
* Add vectors together and return result
*/
function addVectors(v) {
var add = true;
while (add) {
if (v.length == 2) {
add = false;
return mathjs.add(v[0], v[1]);
} else {
result = mathjs.add(v[0], v[1]);
// remove the first vector in array
v.shift();
// replace the new first element in array with result
v[0] = result;
}
}
}
/**
* Displays the output of calculations
*/
function showOutput() {
// hide output container while mathjax renders.
$('#output-container').addClass('invisible');
var result = calculateOutput();
var matrix = result[0];
var vector = result[1];
var matrixRows = matrixToArray(matrix);
var vectorRows = matrixToArray(vector);
// update global result variable
resultEqtn = 'm,' + matrixRows.toString() + ',v,' + vectorRows.toString();
$("#code-to-copy").val(resultEqtn);
matrixString = formatMatrix(matrixRows, ROW_TEMPLATE);
vectorString = sprintf(MATRIX_TEMPLATE, vectorRows[0], vectorRows[1], vectorRows[2]);
$('#matrix-output').html(matrixString);
$('#vector-output').html(vectorString);
MathJax.Hub.Queue(["Typeset", MathJax.Hub, "output-container"]); // typeset calculated result
showEquations();
}
/**
* Defines drag and drop events
*/
$(function() {
var matrix_list = $('.containers').toArray();
var drake = dragula(matrix_list, {
moves: function (el, container, handle) {
// Define dragging via a handle instead of the whole box
return handle.classList.contains('handle');
},
accepts: function(el, target, source, sibling) {
// Don't allow dragging of vectors to matrices container and vice versa
return target.id === source.id;
}
});
drake.on('drag', (eqtn, source) => {
scrollable = false;
eqtn.children[0].children[1].classList.add('held'); // The handle
});
drake.on('dragend', (eqtn) => {
eqtn.children[0].children[1].classList.remove('held'); // The handle
});
drake.on('drop', (eqtn, target_container, source_container, sibling) => {
var eqtnDiv = $(eqtn.children[MATRIX_CHILD_INDEX]);
if (sibling == null) {
// eqtn has been inserted last
if (eqtnDiv.hasClass('matrix')) {
var siblingOrder = currentMatricesOrder.length - 1; // because it's last in order
var siblingDiv = $("div").find("[data-matrix-order='" + siblingOrder + "']");
} else {
var siblingOrder = currentVectorsOrder.length - 1; // because it's last in order
var siblingDiv = $("div").find("[data-vector-order='" + siblingOrder + "']");
}
} else {
// Get the matrix/vector it is being swapped with
var siblingDiv = $(sibling.children[MATRIX_CHILD_INDEX]);
if (eqtnDiv.hasClass('matrix')) {
var siblingOrder = siblingDiv.attr('data-matrix-order');
} else {
var siblingOrder = siblingDiv.attr('data-vector-order');
}
}
// swap
if (eqtnDiv.hasClass('matrix')) {
var matrixOrder = eqtnDiv.attr('data-matrix-order');
// swapping in the array that holds mathjs objects
var tmp = currentMatricesOrder[matrixOrder];
currentMatricesOrder[matrixOrder] = currentMatricesOrder[siblingOrder];
currentMatricesOrder[siblingOrder] = tmp;
// swapping in the array that holds string objects
var tmp = matricesStringFormat[matrixOrder];
matricesStringFormat[matrixOrder] = matricesStringFormat[siblingOrder];
matricesStringFormat[siblingOrder] = tmp;
// swap data-order attributes
eqtnDiv.attr('data-matrix-order', siblingOrder);
siblingDiv.attr('data-matrix-order', matrixOrder);
} else {
// vector
var vectorOrder = eqtnDiv.attr('data-vector-order');
// swapping in the array that holds mathjs objects
var tmp = currentVectorsOrder[vectorOrder];
currentVectorsOrder[vectorOrder] = currentVectorsOrder[siblingOrder];
currentVectorsOrder[siblingOrder] = tmp;
// swapping in the array that holds string objects
var tmp = vectorsStringFormat[matrixOrder];
vectorsStringFormat[matrixOrder] = vectorsStringFormat[siblingOrder];
vectorsStringFormat[siblingOrder] = tmp;
// swap data-order attributes
eqtnDiv.attr('data-vector-order', siblingOrder);
siblingDiv.attr('data-vector-order', vectorOrder);
}
showOutput();
scrollable = true;
});
});
/**
* Converts mathjs matrix to an Array
*/
function matrixToArray(matrix) {
var matrixArray = [[], [], []];
matrix.forEach(function (value, index, m) {
i = index[0];
j = index[1];
// round to 2dp
matrixArray[i][j] = mathjs.round(value, 2);
});
return matrixArray;
}
/**
* Only show equations once they are rendered
*/
function showEquations() {
MathJax.Hub.Queue(
function () {
$('.invisible').removeClass('invisible');
// stop buttons jumping around on load
$('#add-matrix-btn').removeClass('d-none');
$('#add-vector-btn').removeClass('d-none');
$('#copy-eqtn').removeClass('d-none');
$('#remove-all-matrices').removeClass('d-none');
$('#remove-all-vectors').removeClass('d-none');
});
}
/**
* Removes equation and updates output to match
*/
function dismissEquation() {
eqtnToRemove = $(this).next().next(); // div of matrix/vector to remove
if (eqtnToRemove.hasClass('matrix')) {
orderIndex = eqtnToRemove.attr('data-matrix-order');
// remove from order array
currentMatricesOrder.splice(orderIndex, 1);
// remove from string array
matricesStringFormat.splice(orderIndex, 1);
// remove DOM element
eqtnToRemove.parent().remove();
// update data-order attributes
$('div[data-matrix-order]').each(function() {
order = $(this).attr('data-matrix-order');
if (order > orderIndex) {
newOrder = order - 1;
$(this).attr('data-matrix-order', newOrder);
}
});
if (currentMatricesOrder.length == 0) {
$('#remove-all-matrices').attr('disabled', true);
}
} else { //vector
orderIndex = eqtnToRemove.attr('data-vector-order');
// remove from order array
currentVectorsOrder.splice(orderIndex, 1);
// remove from string array
vectorsStringFormat.splice(orderIndex, 1);
// remove DOM element
eqtnToRemove.parent().remove();
// update data-order attributes
$('div[data-vector-order]').each(function() {
order = $(this).attr('data-vector-order');
if (order > orderIndex) {
newOrder = order - 1;
$(this).attr('data-vector-order', newOrder);
}
});
if (currentVectorsOrder.length == 0) {
$('#remove-all-vectors').attr('disabled', true);
}
}
// re-calculate and show output
showOutput();
}
/**
* Populates modal with values of matrix/vector that is to be edited
*/
function populateModalForEditing() {
eqtnToEdit = $(this).next(); // div of matrix/vector to edit
if (eqtnToEdit.hasClass('matrix')) {
$('#matrix-modal-title').html(gettext("Update matrix"));
// replace add button with edit button in modal
$("#add-matrix-from-input").addClass("d-none");
$("#update-matrix").removeClass("d-none");
orderIndex = eqtnToEdit.attr('data-matrix-order');
// get matrix in string form
matrix = matricesStringFormat[orderIndex];
// populate modal values
$('#matrix-row-0-col-0').val(matrix[0][0]);
$('#matrix-row-0-col-1').val(matrix[0][1]);
$('#matrix-row-0-col-2').val(matrix[0][2]);
$('#matrix-row-1-col-0').val(matrix[1][0]);
$('#matrix-row-1-col-1').val(matrix[1][1]);
$('#matrix-row-1-col-2').val(matrix[1][2]);
$('#matrix-row-2-col-0').val(matrix[2][0]);
$('#matrix-row-2-col-1').val(matrix[2][1]);
$('#matrix-row-2-col-2').val(matrix[2][2]);
// add event listener to edit button
$('#update-matrix').click(function() {
updateEquation(eqtnToEdit, orderIndex);
});
} else {
$('#vector-modal-title').html(gettext("Update vector"));
$("#add-vector-from-input").addClass("d-none");
$("#update-vector").removeClass("d-none");
orderIndex = eqtnToEdit.attr('data-vector-order');
// get matrix in string form
vector = vectorsStringFormat[orderIndex];
// populate modal values
$('#vector-row-0').val(vector[0]);
$('#vector-row-1').val(vector[1]);
$('#vector-row-2').val(vector[2]);
// add event listener to edit button
$('#update-vector').click(function() {
updateEquation(eqtnToEdit, orderIndex);
});
}
}
/**
* Updates matrix/vector with new values
*/
function updateEquation(eqtnDiv, orderIndex) {
if (eqtnDiv.hasClass('matrix')) {
// inputs get evaluated as math
matrixArrayMath = getMatrix(true);
// inputs remain as strings for display
matrixArrayString = getMatrix(false);
matricesStringFormat[orderIndex] = matrixArrayString;
// convert to mathjs matrix so we can do calculations
matrix = mathjs.matrix(matrixArrayMath);
currentMatricesOrder[orderIndex] = matrix;
matrixString = formatMatrix(matrixArrayString, ROW_TEMPLATE);
// hide div until math has been typeset
eqtnDiv.addClass("d-none")
eqtnDiv.html(matrixString);
} else {
// inputs get evaluated as math
vectorArrayMath = [
[mathjs.evaluate($('#vector-row-0').val())],
[mathjs.evaluate($('#vector-row-1').val())],
[mathjs.evaluate($('#vector-row-2').val())]
];
// inputs remain as strings for display
vectorArrayString = [
[$('#vector-row-0').val()],
[$('#vector-row-1').val()],
[$('#vector-row-2').val()]
];
vectorsStringFormat[orderIndex] = vectorArrayString;
// convert to mathjs matrix so we can do caluclations
vector = mathjs.matrix(vectorArrayMath);
currentVectorsOrder[orderIndex] = vector;
vectorString = sprintf(
MATRIX_TEMPLATE,
vectorArrayString[0],
vectorArrayString[1],
vectorArrayString[2]
);
// hide div until math has been typeset
eqtnDiv.addClass("d-none")
eqtnDiv.html(vectorString);
}
MathJax.Hub.Queue(["Typeset", MathJax.Hub, eqtnDiv[0]]); // typeset the updated equation
eqtnDiv.removeClass("d-none");
showOutput();
}
/**
* Checks user input as they are typing.
* Highlights input box red if the input is invalid and disables the add button.
*/
function validateInput() {
var input = $(this).val();
var success = false;
try {
inputEvaluated = mathjs.evaluate(input);
mathjs.number(inputEvaluated);
success = true;
}
catch {
$(this).addClass('input-error');
$('.add-from-input').prop('disabled', true);
}
if (success) {
$(this).removeClass('input-error');
}
// if there are no input erros, enable add button
if ($('.input-error').length == 0) {
$('.add-from-input').prop('disabled', false);
}
}
/** Removes all equations and re-calculates output */
function removeAllMatrices() {
$(".matrix").parent().remove();
currentMatricesOrder = [];
matricesStringFormat = [];
$('#remove-all-matrices').attr('disabled', true);
// re-calculate and show output
showOutput();
}
/** Removes all equations and re-calculates output */
function removeAllVectors() {
$(".vector").parent().remove();
currentVectorsOrder = [];
vectorsStringFormat = [];
$('#remove-all-vectors').attr('disabled', true);
// re-calculate and show output
showOutput();
}