/* * This groovy script converts a JFlap jff file representing a finite automaton * to LaTeX file depicting the automaton graphically using TikZ. * * Copyright (c) 2009, Andrew Mertz and William Slough * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ // Define a few constants scale = 1.0 // 1 pixel in JFlap = scale points in LaTeX gridsize = 20.0 // gridsize is measured in pixels turingBlank = '$\\boxvoid$' // the symbol to be used for a blank in a TM blank = '$\\lambda$' // the symbol to be used for blank otherwise // Define a function that cleans up text that could be "bad" for LaTeX to // ingest String fixText(String text) { if(text) { text = text.replace('\\', '$\\backslash$') text = text.replace('~', '$\\sim$') text = text.replaceAll('[$%^&_#{}]', {'\\'+ it[0]}) } return text } // Setup the command line options // TODO: add switch for file output (maybe) cmdLine = new CliBuilder(usage: 'JFlap2TikZ [-g] [-h] [-r] [-s scale] [-z size] filename') cmdLine.h(longOpt:'help', 'Show usage information and quit') cmdLine.g(longOpt:'grid', 'Round positions so that they are on a grid') cmdLine.z(longOpt:'gridsize', args:1, "Set the spacing of the grid (default is ${gridsize})") cmdLine.s(longOpt:'scale', args:1, "1 pixel in JFlap = scale points in LaTeX (default is ${scale})") cmdLine.r(longOpt:'rotate', "rotate labels along edges") // Parse the command line options options = cmdLine.parse(args) // If there is a parse error, quit. Note usage information will already have // been displayed. if(!options) { System.exit(1) } // If the user has asked for help print the usage information and quit if(options.h) { cmdLine.usage() System.exit(0) } grid = false if(options.g) { grid = true } // Verify that scale and gridsize options are valid (positive doubles) try{ if(options.z) { gridsize = Double.valueOf(options.z) grid = true // turn on the grid if (gridsize <= 0) { throw new NumberFormatException(); } } } catch(NumberFormatException ex) { System.err.println "error: gridsize must be a positive number" cmdLine.usage() System.exit(2) } try{ if(options.s) { scale = Double.valueOf(options.s) if (scale <= 0) { throw new NumberFormatException(); } } } catch(NumberFormatException ex) { System.err.println "error: scale must be a positive number" cmdLine.usage() System.exit(2) } // Check that we have a filename if(options.arguments().size() != 1) { cmdLine.usage() System.exit(3) } // Try to get the text from the file try { body = new File(options.arguments()[0]).text } catch(IOException ex) { System.err.println "error: unable to read ${args[0]}" System.exit(4) } // Parse the XML try { structure = new XmlParser().parseText(body) } catch(Exception ex) { System.err.println "error: parsing XML" System.exit(5) } // Check the type to be sure it is a machine that can be converted type = structure.type.text() if(type != "fa" && type != "pda" && type != "turing") { println "error: unsupported machine type --- only FAs, PDAs and Turing machines can be converted" System.exit(6); } // If the automaton is a Turing machine, get the number of tapes try { tapes = Integer.valueOf(structure.tapes.text()) } catch (Exception ex) { tapes = 1 } // Find the automaton in the XML tree automaton = structure.automaton if(!automaton) { automaton = structure } // Print the LaTeX header println """\\documentclass{article} \\usepackage{tikz} \\usetikzlibrary{automata,positioning}${type == "turing" ? "\n\\usepackage{mathabx}\n" : ""} \\usepackage[graphics,tightpage,active,pdftex]{preview} \\setlength{\\PreviewBorder}{5pt} \\PreviewEnvironment{tikzpicture} \\begin{document} \\begin{tikzpicture}[>=latex, shorten >=1pt${options.r ? "" : ", auto"}]""" // For each state in the XML define a TikZ node and record their position statePosition = [:] automaton.state.each { // Get the x and y value of the position, correcting for the change in // coordinate systems x = Double.valueOf(it.x.text()) * scale y = -Double.valueOf(it.y.text()) * scale if(grid) { x = Math.round(x / gridsize) * gridsize y = Math.round(y / gridsize) * gridsize } // record position statePosition[it.@id] = [x,y] // Output the TikZ version of this state println " \\draw (${x}pt, ${y}pt)" + "node[state${it.initial ? ", initial, initial text =" : ""}" + "${it.get("final") ? ", accepting" : ""}]" + "(${fixText(it.@id)}){\$q_{${fixText(it.@id)}}\$};" } // Define a function that makes a key based on the end points of a transition // without taking into account the order of the keys. In other words, edge // (a,b) results in the same key as the edge (b,a). String makeKey(String a, String b) { return a < b ? "${a},${b}" : "${b},${a}" } // Scan transitions and create their labels. Also, check to see if there are // edges going in both directions. doubleEdges = [:] edgeLabels = [:] automaton.transition.each { orderedKey = [it.from.text(), it.to.text()] // Have we seen the reverse of this edge before? if(edgeLabels.get([it.to.text(), it.from.text()]) && it.to.text() != it.from.text()) { doubleEdges.put(makeKey(it.from.text(), it.to.text()), true) } // Get the read symbol. If it is blank use lambda for the symbol. if(!(readSymbol = fixText(it.read.text()))) { readSymbol = blank } // Get the pop and push symbols if the machine is a PDA if(type == "pda") { if(!(popSymbol = fixText(it.pop.text()))) { popSymbol = blank } if(!(pushSymbol = fixText(it.push.text()))) { pushSymbol = blank } } // If the machine is a Turing machine, get all of the read, write, and move // information if(type == "turing") { readSymbols = [] it.read.each{r-> readSymbols << (r.text() ? fixText(r.text()) : turingBlank) } writeSymbols = [] it.write.each{w-> writeSymbols << (w.text() ? fixText(w.text()) : turingBlank) } moves = [] it.move.each{m-> moves << fixText(m.text()) } } // Make the label for this transition if(type == "fa") { label = readSymbol; } else if(type == "pda") { label = "${readSymbol}, ${popSymbol}; ${pushSymbol}" } else if(type == "turing") { // Merge the read, write and move lists into one label label = [] readSymbols.eachWithIndex{read, i-> label << "${read} : ${writeSymbols[i]}, ${moves[i]}" } label = label.join('$|$') } // Add this edge's label to the edge label list if((oldLabel = edgeLabels.get(orderedKey))) { oldLabel << label } else { edgeLabels.put(orderedKey, [label]) } } // For each transition draw an edge edgeLabels.each { // Turn the label into a string if(type == "fa") { label = it.value.join(", ") } else { label = it.value.join("\\\\ ") } // Construct the options for the node that will hold the label nodeOptions = [] if(type != "fa" && it.value.size() > 1) // Are line breaks needed? { nodeOptions << "text width=${tapes*2}cm,text centered" } if(options.r && it.key[0] != it.key[1]) // Does the text need to be rotated? { nodeOptions << "sloped" // In the case of rotations auto positioning is not used. Thus, an anchor // point for the node must be given. if(statePosition[it.key[0]][0] < statePosition[it.key[1]][0]) { nodeOptions << "anchor=south" } else { nodeOptions << "anchor=north" } } // Turn the node options into a string. If there are no options then use the // empty string. Otherwise join the option with commas and surround them with // square brackets. if(nodeOptions.size() > 0) { nodeOptions = "[${nodeOptions.join(",")}]" } else { nodeOptions = "" } // TODO: When the align option enters the main TikZ release we should switch // to using it rather than a fixed text width. // TODO: Find a way to not to have to use a fixed text width or at least tune // it in some way. println " \\path[->] (${it.key[0]}) edge" + (it.key[0] == it.key[1] ? "[loop above]" : "") + // Do we have a self loop? (doubleEdges.get(makeKey(it.key[0], it.key[1])) ? "[bend left]" : "") + // Do we have more than one edge? If so add a bend. " node${nodeOptions}" + "{${label}}(${it.key[1]});" } // Print the LaTeX footer println """\\end{tikzpicture} \\end{document}"""