/** @module index */
import * as linalg from './linalg.js';
import * as consts from './consts.js';
import * as hist from './history.js';
import * as drawing from './drawing.js';
import * as compute from './compute.js';
import * as graph_ops from './graph_ops.js';
import * as menus from './menus.js';
import * as permalink from './permalink.js';
import * as util from './util.js';
import * as ui_setup from './ui_setup.js';
import * as regex from './regex.js';
// if not in browser, don't run
if (typeof document !== 'undefined') {
document.addEventListener('DOMContentLoaded', init);
window.addEventListener('resize', () => drawing.draw(graph));
}
let graph = {}; // global graph
/** handles double click */
function bind_double_click() {
drawing.get_canvas().addEventListener('dblclick', e => { // double click to create vertices
if (e.movementX || e.movementY) {
return;
} // shifted, don't create
const [x, y] = drawing.event_position_on_canvas(e);
const v = drawing.in_any_vertex(graph, x, y);
if (v) {
graph_ops.toggle_final(graph, v);
} else {
// use the default radius if there is no vertex for reference
const radius = (Object.keys(graph).length) ? Object.values(graph)[0].r : consts.DEFAULT_VERTEX_RADIUS;
graph_ops.create_vertex(graph, x, y, radius);
}
});
}
/**
* shift the entire graph by dx and dy
* @param {Object} e - mousemove event
*/
function drag_scene(e) {
const dx = e.movementX, dy = e.movementY;
for (const vertex of Object.values(graph)) {
vertex.x += dx*window.devicePixelRatio;
vertex.y += dy*window.devicePixelRatio; // calculated in canvas pixel, which is some multiple of window pixel
}
drawing.draw(graph);
}
/**
* builds a drag vertex callback function
* @param {string} v - name of the vertex to be dragged
* @returns {function} a callback function to handle dragging a vertex
*/
function higher_order_drag_vertex(v) {
const vertex = graph[v];
let moved = false;
drawing.get_canvas().addEventListener('mouseup', () => { // additional event listener to hist.push_history
if (moved) {
hist.push_history(graph);
}
}, { once:true }); // save once only
return e => {
[vertex.x, vertex.y] = drawing.event_position_on_canvas(e);
drawing.draw(graph);
moved = true;
};
}
/**
* creates a callback function to handle edge creation animation
* @param {string} v - the vertex from which the edge is stemmed from
* @returns {function} a function that handles drawing an edge from v on mousedrag
*/
function higher_order_edge_animation(v) {
const vertex = graph[v]; // convert name of vertex to actual vertex
const canvas = drawing.get_canvas();
const ctx = canvas.getContext('2d');
const cached_canvas = canvas.cloneNode();
cached_canvas.getContext('2d').drawImage(canvas, 0, 0); // save the original image
const restore = () => { // helper to restore to the orignal canvas
ctx.clearRect(0, 0, canvas.width, canvas.height); // clear
ctx.drawImage(cached_canvas, 0, 0); // restore
};
let has_left_before = false;
let angle1, angle2;
canvas.addEventListener('mouseup', e => { // additional event listener to restore canvas and snap to vertex
const [x, y] = drawing.event_position_on_canvas(e);
const cur_v = drawing.in_any_vertex(graph, x, y);
const cur_vertex = graph[cur_v];
restore();
if (cur_v && has_left_before) {
angle2 = Math.atan2(y-cur_vertex.y, x-cur_vertex.x);
graph_ops.create_edge(graph, v, cur_v, angle1, angle2); // snap to the other vertex
}
}, { once:true }); // snap once only
return e => {
const [x, y] = drawing.event_position_on_canvas(e);
if (drawing.in_any_vertex(graph, x, y) === v) {
return;
} // haven't left the vertex yet
// now we are away from the vertex
if (!has_left_before) {
has_left_before = true;
angle1 = Math.atan2(y-vertex.y, x-vertex.x);
}
restore();
const [truncate_x, truncate_y] = linalg.normalize([x-vertex.x, y-vertex.y], vertex.r);
drawing.draw_arrow([vertex.x+truncate_x, vertex.y+truncate_y], [x, y]);
};
}
/**
* creates a callback function that handles dragging an edge
* @param {Object} edge - the edge you are dragging
* @returns {function} a callback function that handles dragging an edge
*/
function higher_order_drag_edge(edge) {
const s = graph[edge.from];
let moved = false;
drawing.get_canvas().addEventListener('mouseup', () => { // additional event listener to hist.push_history
if (moved) {
hist.push_history(graph);
}
}, { once:true }); // save once only
return e => {
const mouse_pos = drawing.event_position_on_canvas(e);
const [start, end] = drawing.compute_edge_start_end(graph, edge);
const mid = linalg.sub(mouse_pos, start);
const v1 = linalg.sub(end, start);
const v2 = linalg.normalize(linalg.normal_vec(v1), s.r); // basis
const [inv_v1, inv_v2] = linalg.inv(v1, v2);
[edge.a1, edge.a2] = linalg.linear_comb(inv_v1, inv_v2, ...mid); // matrix vector product
drawing.draw(graph);
moved = true;
};
}
/** binds callback functions to the mouse dragging behavior and deletes if event is dropped over trash*/
function bind_drag() {
let mutex = false; // drag lock not activiated
// declare the callbacks as empty function so that intellisense recognizes them as function
let edge_animation = consts.EMPTY_FUNCTION, drag_edge = consts.EMPTY_FUNCTION, drag_vertex = consts.EMPTY_FUNCTION;
const canvas = drawing.get_canvas();
canvas.addEventListener('mousedown', e => {
if (mutex) {
return;
} // something has already bind the mouse drag event
mutex = true; // lock
const [x, y] = drawing.event_position_on_canvas(e);
const clicked_vertex = drawing.in_any_vertex(graph, x, y);
const clicked_edge = drawing.in_edge_text(graph, x, y);
if ((e.button === consts.RIGHT_BTN || e.ctrlKey || e.metaKey) && clicked_vertex) { // right create edge
edge_animation = higher_order_edge_animation(clicked_vertex);
canvas.addEventListener('mousemove', edge_animation);
} else if (e.button === consts.LEFT_BTN) { // left drag
if (clicked_edge) { // left drag edge
drag_edge = higher_order_drag_edge(clicked_edge);
canvas.addEventListener('mousemove', drag_edge);
} else if (clicked_vertex) { // vertex has lower priority than edge
drag_vertex = higher_order_drag_vertex(clicked_vertex); // create the function
canvas.addEventListener('mousemove', drag_vertex);
} else { // left drag scene
canvas.addEventListener('mousemove', drag_scene);
}
}
});
canvas.addEventListener('mouseup', e => {
const [x, y] = drawing.event_position_on_canvas(e);
const drop_vertex = drawing.in_any_vertex(graph, x, y);
const drop_edge = drawing.in_edge_text(graph, x, y);
if (drop_vertex && drawing.over_trash(e)) { // delete vertex if dropped over trash
graph_ops.delete_vertex(graph, drop_vertex);
} else if (drop_edge && drawing.over_trash(e)) { // delete edge if dropped over trash
graph_ops.delete_edge(graph, drop_edge);
} else if (drawing.over_trash(e)) {
delete_graph(); // delete the entire graph if dropped over trash
}
canvas.removeEventListener('mousemove', drag_scene);
canvas.removeEventListener('mousemove', drag_vertex);
canvas.removeEventListener('mousemove', drag_edge);
canvas.removeEventListener('mousemove', edge_animation);
mutex = false; // release the resource
});
}
/** replaces the default context menu */
function bind_context_menu() {
const canvas = drawing.get_canvas();
let last_time_mouse_press = 0;
document.addEventListener('contextmenu', e => e.preventDefault()); // stop contextmenu from showing
canvas.addEventListener('mousedown', e => {
menus.remove_context_menu(); // remove old menu
if (e.button === consts.RIGHT_BTN) {
last_time_mouse_press = e.timeStamp;
}
});
canvas.addEventListener('mouseup', e => {
if (e.timeStamp - last_time_mouse_press > consts.CLICK_HOLD_TIME) {
return;
} // hack
const [x, y] = drawing.event_position_on_canvas(e);
const v = drawing.in_any_vertex(graph, x, y);
const edge = drawing.in_edge_text(graph, x, y);
if (v) {
menus.display_vertex_menu(graph, v, e.clientX, e.clientY);
} else if (edge) {
menus.display_edge_menu(graph, edge, e.clientX, e.clientY);
}
});
}
const computations = []; // we want the computations to be persistent
/** binds each machine input to the run_input function */
export function bind_run_input() {
const input_divs = document.getElementsByClassName('machine_input');
const new_input_idx = input_divs.length - 1;
const new_input = input_divs[new_input_idx];
const textbox = new_input.querySelector('.machine_input_text');
const run_btn = new_input.querySelector('.run_btn');
run_btn.addEventListener('click', () => {
new_input.style.backgroundColor = consts.SECOND_BAR_COLOR;
drawing.highlight_states(graph, []); // clear the highlighting
computations[new_input_idx] = compute.run_input(graph, menus.machine_type(), textbox.value); // noninteractive
// eslint-disable-next-line no-unused-vars
const { value: output, _ } = computations[new_input_idx].next(); // second value always true when noninteractive
if(menus.machine_type() === consts.MACHINE_TYPES.Moore || menus.machine_type() === consts.MACHINE_TYPES.Mealy) {
window.setTimeout(() => alert(output), 0); // alert after the color change
} else {
new_input.style.backgroundColor = output ? consts.ACCEPT_COLOR : consts.REJECT_COLOR;
}
computations[new_input_idx] = undefined;
});
const step_btn = new_input.querySelector('.step_btn');
step_btn.addEventListener('click', () => {
new_input.style.backgroundColor = consts.SECOND_BAR_COLOR;
if (!computations[new_input_idx]) {
// last param true for interactive computation
computations[new_input_idx] = compute.run_input(graph, menus.machine_type(), textbox.value, true);
}
const { value: output, done } = computations[new_input_idx].next();
if (done) {
if(menus.machine_type() === consts.MACHINE_TYPES.Moore || menus.machine_type() === consts.MACHINE_TYPES.Mealy) {
window.setTimeout(() => alert(output), 0); // alert after the color change
} else {
new_input.style.backgroundColor = output ? consts.ACCEPT_COLOR : consts.REJECT_COLOR;
}
computations[new_input_idx] = undefined;
}
});
const reset_btn = new_input.querySelector('.reset_btn');
reset_btn.addEventListener('click', () => {
computations[new_input_idx] = undefined;
new_input.style.backgroundColor = consts.SECOND_BAR_COLOR;
drawing.highlight_states(graph, []); // clear the highlighting
});
}
/** offers ctrl-z and ctrl-shift-z features */
function bind_undo_redo() {
document.addEventListener('keydown', e => {
if (e.code !== 'KeyZ' || e.altKey) { // must not have alt pressed but must have 'z' pressed
return;
}
e.preventDefault(); // prevent input undo
if ((e.ctrlKey || e.metaKey) && e.shiftKey) {
graph = hist.redo();
} else if ((e.ctrlKey || e.metaKey)) {
graph = hist.undo();
}
drawing.draw(graph);
});
}
/** zooming in and out */
function bind_scroll() {
drawing.get_canvas().addEventListener('wheel', e => {
e.preventDefault(); // prevent browser scrolling or zooming
const [x, y] = drawing.event_position_on_canvas(e);
const zoom_const = 1 - consts.ZOOM_SPEED*e.deltaY;
for (const vertex of Object.values(graph)) {
vertex.x = x + zoom_const*(vertex.x-x);
vertex.y = y + zoom_const*(vertex.y-y);
vertex.r *= zoom_const;
}
drawing.draw(graph);
});
}
function delete_graph() {
if (!Object.keys(graph).length) { // nothing to delete
return;
}
graph = {};
drawing.draw(graph);
hist.push_history(graph);
}
/** press dd does delete */
function bind_dd() {
util.on_double_press('KeyD', delete_graph);
}
function hash_change_handler() {
if (window.location.hash.length > 1) {
const graph_str = window.location.hash.slice(1);
const select = document.getElementById('select_machine');
if (permalink.serialize(select.value, graph) === graph_str) {
// debounce two types of events
// 1. the permalink generation will trigger a hash change event, which we do not want to handle
// 2. the user might have inputed the same graph string, so we prevent duplicate history by not hanlding
return;
}
let type;
[type, graph] = permalink.deserialize(graph_str);
select.value = type;
hist.set_history_keys(type); // set the history keys to the correct machine type
hist.push_history(graph); // save the graph to history
menus.display_UI_for(type); // change the UI elements manually
refresh_graph(); // draw the graph
}
}
/** draw the first graph (possibly by deserializing the permalink) */
function init_graph() {
hash_change_handler();
refresh_graph();
}
/** get the newest graph from history and draw it */
function refresh_graph() {
graph = hist.retrieve_latest_graph();
drawing.draw(graph);
}
/** handle switching machine type event */
function bind_switch_machine() {
const select = document.getElementById('select_machine');
select.value = consts.DEFAULT_MACHINE; // set to default machine here too
select.addEventListener('change', e => {
hist.set_history_keys(e.target.value);
refresh_graph(); // switching graph
menus.display_UI_for(e.target.value);
history.replaceState(undefined, undefined, '#'); // clear the permalink
});
// clear the partial computations when user switches machines
document.getElementById('select_machine').addEventListener('change', () => computations.fill(undefined));
}
/** handles the NFA to DFA button */
function bind_machine_transform() {
const NFA_2_DFA_btn = document.getElementById('NFA_to_DFA');
NFA_2_DFA_btn.addEventListener('click', () => {
graph = graph_ops.NFA_to_DFA(graph);
drawing.draw(graph);
hist.push_history(graph);
});
}
/** hook up the save button */
function bind_save_drawing() {
const save_btn = document.getElementById('save_machine');
save_btn.addEventListener('click', () => drawing.save_as_png(graph));
}
/** button to generate permanent link */
function bind_permalink() {
const permalink_btn = document.getElementById('permalink');
permalink_btn.addEventListener('click', () => {
const select = document.getElementById('select_machine');
const graph_str = permalink.serialize(select.value, graph);
history.replaceState(undefined, undefined, '#'+graph_str);
navigator.clipboard.writeText(window.location.href)
.then(() => alert('Permalink copied to clipboard!'));
});
window.addEventListener('hashchange', hash_change_handler);
}
/** change cursor style when hovering over certain elements */
function bind_mousemove() {
const canvas = drawing.get_canvas();
canvas.addEventListener('mousemove', e => {
const [x, y] = drawing.event_position_on_canvas(e);
if (drawing.in_edge_text(graph, x, y) !== null || drawing.in_any_vertex(graph, x, y) !== null ||
drawing.over_trash(e)) {
canvas.style.cursor = 'pointer';
} else {
canvas.style.cursor = 'auto';
}
});
}
/** bind context menu for side nav bar and secondary side navbar */
function bind_context_menu_navbar(){
const navbar = document.querySelector('.nav')
const secondBar = document.querySelector('#secondbar')
navbar.addEventListener('click', () => {menus.remove_context_menu()})
secondBar.addEventListener('click', () => {menus.remove_context_menu()})
/*
for(var btns of navbar){
btns.addEventListener('click', () => {remove_context_menu()})
}
*/
}
function bind_regex() {
// let [input_field, open_btn, close_btn, union_btn, concat_btn, kleene_btn, sigma_btn, empty_btn, convert] = regex.create_buttons();
regex.create_buttons();
const convert_to_nfa_btn = document.getElementById('convert_to_nfa');
convert_to_nfa_btn.addEventListener('click', () => {
console.log(document.getElementById('regex_string').value);
let inputString = document.getElementById('regex_string').value
inputString = inputString.replace(/\s/g, '');
if (regex.isValidRegex(inputString)) {
graph = regex.process_string(inputString);
menus.display_UI_for('NFA');
document.getElementById('select_machine').value = 'NFA';
drawing.draw(graph);
// hist.push_history(graph); NEED TO IMPLEMENT HISTORY BEFORE UNCOMMENTING
} else {
alert("Invalid regular expression.")
}
});
const input_field = document.getElementById('regex_string');
input_field.addEventListener('keypress', (e) => {
if (e.key === "Enter") {
console.log(document.getElementById('regex_string').value);
let inputString = document.getElementById('regex_string').value
inputString = inputString.replace(/\s/g, '');
if (regex.isValidRegex(inputString)) {
graph = regex.process_string(inputString);
menus.display_UI_for('NFA');
document.getElementById('select_machine').value = 'NFA';
drawing.draw(graph);
// hist.push_history(graph); NEED TO IMPLEMENT HISTORY BEFORE UNCOMMENTING
} else {
alert("Invalid regular expression.")
}
}
})
}
/** run after all the contents are loaded to hook up callbacks */
function init() {
bind_switch_machine();
bind_double_click();
bind_drag();
bind_context_menu();
bind_context_menu_navbar();
bind_machine_transform();
bind_save_drawing();
bind_undo_redo();
bind_scroll();
bind_dd();
bind_permalink();
bind_mousemove();
ui_setup.bind_plus_minus();
ui_setup.add_input_bar(); // called so one input bar appears on opening of homepage
ui_setup.htmlSetUp(); // initiate eventlisteners for sidenavbar, second sidenavbar, and popup tutorial
bind_regex();
init_graph(); // leave this last since we want it to override some of the above
}