Source: latex.js

/** @module latex */

// -------------------------------------------------------------
// @author Meruzhan Sargsyan
//
// A module used to export the graph as text used in tikzpicture
// for easily exporting graphs into latex files
// -------------------------------------------------------------

//----------------------------------------------
// Testing Notes:
// - clipboard only available in secure contexts
//----------------------------------------------

//----------------------------------------------
// Current TODO:
// 1. Overlapping labels for self loops
// -Done Single state machine does not position 
//----------------------------------------------

import * as consts from './consts.js';
import * as linalg from './linalg.js';
import * as drawing from './drawing.js';

let debug = false; // change this to enable/disable logging
const tikzLabel = {}; // maps name of vertex in graph to it's tikz label used for reference

/**
 * compresses graph to tikz space 
 * @param {String} type - type of graph (DFA, NFA, ...)
 * @param {Array<Object>} states - the states of the graph
 * @returns {Array<String>} formatted positions of states
 */
function compress_planar(states) {
  const distance = 6;

  let centroidX = 0, centroidY = 0;
  let n = states.length;

  let output = Array(n);

  for(let i = 0; i < n; i++) {
    let state = states[i];
    centroidX += state.x;
    centroidY += state.y;
    output[i] = [state.x, state.y];
  }
  if(debug) {
    console.log(output);
  }

  centroidX /= n;
  centroidY /= n;
  let center = [centroidX, centroidY];

  let maxDist = Number.MIN_VALUE;
  for(let i = 0; i < n; i++) {
    output[i] = linalg.sub(output[i], center);
    maxDist = Math.max(maxDist, linalg.vec_len(output[i]));
  }

  let scaleFactor = distance / (2 * maxDist);
  let formatted = output.map((v) => {
    let scaled = linalg.scale(scaleFactor, v);
    return `(${scaled[0].toFixed(2)},${-1 * scaled[1].toFixed(2)})`;
  });

  if(debug) {
    console.log(formatted);
  }
  return formatted;
}

/**
 * Computes the type of a given state 
 * @param {Object} state
 * @returns {String} tikz labels for the type of state
 */
function get_state_type(state) {
  let inner = 'state,';
  if(state.is_start) {
    inner += 'initial,';
  }
  if(state.is_final) {
    inner += 'accepting,';
  }

  return inner;
}

/**
 * gives the position to place label at
 * @param {Object} edge 
 * @return {String} position of label around edge 
 */
function get_label_pos(graph, edge) {
  if(debug) {
    if(edge.from !== edge.to) {
      console.log('Edge is not a self loop');
    }
  }
  
  let [v1, v2, mid] = drawing.compute_edge_geometry(graph, edge);

  // keep in mind that html canvas grows down in y values
  if(mid[1] > v1[1] && mid[1] > v2[1]) {
    // control point below both anchors
    return 'below';
  } else if(mid[1] < v1[1] && mid[1] < v2[1]) {
    // control point above both anchors
    return 'above';
  }
  // control point in between the anchors
  if (mid[0] > v1[0] && mid[0] > v2[0]) {
    return 'right';
  } if(mid[0] < v1[0] && mid[0] < v2[0]) {
    return 'left';
  }

  return 'above';
}

/**
 * converts an edge to tikz string representation
 * @param {String} type - type of graph (DFA, NFA, ...)
 * @param {Object} edge - edge to convert to string
 * @param {String} labelPos - where to position label on edge
 * @returns {String} - tikz string representaiton of edge
 */
function edge_to_string(graph, type, edge) {
  if(debug) {
    console.log(edge);
  }
  let labelPos = get_label_pos(graph, edge);
  let bendAngle = Math.floor(edge.a2) * consts.LATEX_ANGLE_SCALE;
  let inner = `bend right=${bendAngle}`;
  let label = `${edge.transition}`; 

  if(edge.from === edge.to) {
    inner = `loop ${labelPos}`;
  }

  switch (type) {
  case 'PDA':
    label += `,${edge.pop_symbol} \\rightarrow ${edge.push_symbol}`.replaceAll('$', '\\$');
    break;
  case 'Turing':
    label += ` \\rightarrow ${edge.push_symbol}, ${edge.move}`.replaceAll('$', '\\$');
    break;
  default:
    break;
  }

  let output = `(${tikzLabel[edge.from]}) edge [${inner}] node[${labelPos}] {$${label}$} (${tikzLabel[edge.to]})\n`;
  return output.replaceAll(consts.EMPTY_SYMBOL, '\\epsilon').replaceAll(consts.EMPTY_TAPE, '\\square');
}

/**
 * @param {Object} graph - graph to be converted to latex
 * @return {String} representation of graph in latex tikzpicture
 */
export function serialize(type, graph) {
  // setup
  let distance = 2;

  let output = `\\begin{tikzpicture}[->,>=stealth\',shorten >=1pt, auto, node distance=${distance}cm, semithick]\n`;
  output += '\\tikzstyle{every state}=[text=black, fill=none]\n';

  // initializing nodes
  let states = Object.values(graph);
  states.sort((a,b) => a.x - b.x); // sorts the states from left to right

  let statePositions = compress_planar(states);

  if(states.length === 1) {
    statePositions = ["(0,0)"];
  }

  let start = states[0];
  let inner = get_state_type(start);

  for(let i = 0; i < states.length; i++) {
    let current = states[i];
    tikzLabel[current.name] = `s${i}`;
    inner = get_state_type(current);
    let position = statePositions[i];
    output += `\\node[${inner}] (${tikzLabel[current.name]}) at ${position} {$${current.name}$};\n`;
  }

  output += '\n';
  output += '\\path\n';

  for(let i = 0; i < states.length; i++) {
    let current = states[i];
    let edges = current.out; // array of edges

    for(let j = 0; j < edges.length; j++) {
      let edge = edges[j];
      output += edge_to_string(graph, type, edge);
    }
  }
  output += ';\n';

  output += '\\end{tikzpicture}';

  if(debug) {
    console.log(output);
  }

  return output;
}