En
el post anterior vimos cómo generar un diagrama Nassi-Schneiderman con tablas HTML. Hicimos un generador abstracto que nos permitiría hacer implementaciones alternativas, es decir, el diseño es modularizable.
trait Generator[T] {
def nullResult:T
def generate(o:Option[Node]):T = o match {
case Some(x) => generate(x)
case _ => nullResult
}
def generate(n:Node):T
}
En nuestro generador de HTML, el parámetro de tipo fue
String, naturalmente, ya que generamos HTML con Strings. ¿Habrá sido una exageración parametrizar ese tipo? ¿Se les ocurre un generador que use un tipo diferente a String?
¿Y por qué no generar el diagrama como imagen? Un poco de AWT no hace mal. Mi nueva implementación usa
java.awt.Image. Así es como se ve el nuevo diagrama (a partir del mismo código usado como entrada en el post anterior):
Creo que no hace falta explicar nada, el diagrama habla por sí mismo... sólo acotar que, en los IF que no tienen sección ELSE, en vez de dividir el cuadro por la mitad, hice un 90%/10% para evitar que se ocupe mucho espacio con la parte vacía. Faltan algunas mejoras, como incluir el nombre del módulo, los parámetros y la sección de declaraciones; y parametrizar el ancho de la imagen, que ahora está hardcodeado. En cuanto al alto, debería poder ser calculado según el largo del código, pero no lo resolví y por ahora también está hardcodeado.
Definí las siguientes clases auxiliares inmutables, porque esto tiene uso intensivo de "puntos" y "áreas":
case class Point(x:Int, y:Int)
case class Area(start:Point, end:Point) {
lazy val width = end.x - start.x
lazy val height = end.y - start.y
lazy val middleX:Int = start.x + (width / 2)
lazy val middleTop = Point(middleX, start.y)
lazy val middleBottom = Point(middleX, end.y)
lazy val leftBottom = Point(start.x, end.y)
lazy val leftTop = start
lazy val rightTop = Point(end.x, start.y)
}
Los valores definidos en
Area son útiles para que el código sea más claro. Si fueran funciones, el código se ejecutaría en cada invocación. Al ser
val, el código se ejecuta a lo sumo una vez: el valor queda "recordado" para subsiguientes referencias. Y son
lazy para que no se ejecuten hasta ser referenciados.
case object Area {
def apply(a1:Area, a2:Area):Area =
Area(Point(a1.start.x min a2.start.x, a1.start.y min a2.start.y),
Point(a1.end.x max a2.end.x, a1.end.y max a2.end.y))
}
Con esto logramos que
Area pueda ser usado como función, pasando dos áreas como argumentos. El resultado será un área que abarque completamente a esas dos. Esto fue necesario para, luego de procesar ambas secciones de un IF, obtener el área completa de la estructura (ya que cada sección tiene una altura independiente, según el código contenido).
Finalmente, el generador:
trait ImageGenerator extends Generator[Image] {
def imageWidth:Int
val totalWidth = imageWidth
val imageHeight = 800
def nullResult = null
def generate(n:Node):Image = {
val img = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_BYTE_BINARY)
val g = img.createGraphics
g.setPaint(Color.WHITE)
g.fillRect(0, 0, imageWidth, imageHeight)
g.setPaint(Color.BLACK)
(new GraphicsImageGenerator(g)).generate(n, totalWidth - 20, Point(10,20))
img
}
}
El método abstracto
imageWidth deberá ser implementado por quien use este trait, para indicar el ancho de imagen deseado. La altura en cambio, viene horriblemente hardcodeada de fábrica, por ahora. Obviamente esto no es todo, el trabajo sucio lo hace otra clase, a la que llamé
GraphicsImageGenerator. El método
generate crea la
Image que vamos a retornar. Inicializa el correspondiente objeto
Graphics y se lo pasa a una nueva instancia de
GraphicsImageGenerator. Ésta también define un método
generate pero requiere argumentos adicionales: aparte del
Node, necesita saber con qué ancho cuenta para dibujarlo, y a partir de qué punto. Estos valores se calcularán para cada nodo a dibujar, aquí sólo se envían los valores iniciales que corresponden al nodo raíz del AST.
Y ahora sí, les dejo esa clase para que escudriñen si tienen ganas.
class GraphicsImageGenerator(val g:Graphics2D)
{
val lineHeight = 11
val stmtSep = 3
val stmtHeight = 15
val loopThickness = 15
private def wrap(s:String, w:Int) = (new LineBreaker(s, w, g.getFontMetrics)).getSubtrings
private def widthOf(ss:List[String]) = ((ss map g.getFontMetrics.stringWidth) :\ 0)(_ max _)
private def drawRect(a:Area) = g.drawRect(a.start.x, a.start.y, a.end.x - a.start.x, a.end.y - a.start.y)
private def drawLine(p1:Point, p2:Point) = g.drawLine(p1.x, p1.y, p2.x, p2.y)
private def drawJustified(text:String, w:Int, p:Point, drawRect:Boolean)(fx:(Int, Int, Int) => Int) : Area = {
val substrings = wrap(text, w)
var y = p.y + stmtHeight
substrings foreach { s =>
g.drawString( s, fx(p.x, g.getFontMetrics.stringWidth(s), w), y)
y += lineHeight
}
Area(p, Point(p.x + w, y))
}
private def drawLeftJustified(text:String, w:Int, p:Point) = drawJustified(text, w, p, false) {
(x:Int, textWidth:Int, w:Int) => x
}
private def drawRightJustified(text:String, w:Int, p:Point) = drawJustified(text, w, p, false) {
(x:Int, textWidth:Int, w:Int) => (x + w) - textWidth
}
private def drawCentered(text:String, w:Int, p:Point) = drawJustified(text, w, p, true) {
(x:Int, textWidth:Int, w:Int) => (x + (w / 2)) - (textWidth / 2)
}
def generate(n:Node, w:Int, p:Point):Point =
n match {
case stmt:Stmt =>
drawLeftJustified(stmt.s, w, p).leftBottom
case loop:Loop =>
val areaLoop = drawLeftJustified(loop.head, w, p)
val startCode = Point(p.x + loopThickness + 2, areaLoop.end.y + 2)
val loopBodyBottom = generateCode(loop.body, (w - 4) - loopThickness, startCode)
val endPoint = Point(areaLoop.end.x, loopBodyBottom.y)
drawRect(Area(startCode, endPoint))
drawRect(Area(areaLoop.leftTop, endPoint))
Point(p.x, endPoint.y)
case cond:Cond =>
val areaCond = drawCentered(cond.c, w, p)
drawRect(areaCond)
val sepStart = if(cond.cElse.isEmpty) Point(p.x + (w * .9).asInstanceOf[Int], areaCond.end.y) else areaCond.middleBottom
val thenWidth = if(cond.cElse.isEmpty) (w * .9).asInstanceOf[Int] - 4 else (w/2) - 4
drawLine(areaCond.leftTop, sepStart)
drawLine(areaCond.rightTop, sepStart)
val pointThen = generateCode(cond.cThen, thenWidth, Point(p.x + 2, areaCond.end.y + 2))
val pointElse = generateCode(cond.cElse, (w/2) - 4, Point(p.x + 2 + (w/2), areaCond.end.y + 2))
val areaCode = Area(areaCond.leftBottom, Point(areaCond.end.x, pointThen.y max pointElse.y))
drawRect(areaCode)
drawLine(sepStart, Point(sepStart.x, areaCode.end.y))
areaCode.leftBottom
case block:Block =>
val codeEndPoint = generateCode(block.content, w - 4, Point(p.x + 2, p.y + 2))
val endPoint = block.exceps match {
case None => codeEndPoint
case Some(es) => generate(es, w - 4, codeEndPoint)
}
drawRect(Area(p, Point(p.x + w, endPoint.y)))
endPoint
case excepSection:ExcepSection =>
generateCode(excepSection.ee, w, p)
case whenExcep:WhenExcep =>
val areaWhen = drawLeftJustified(whenExcep.e, w - loopThickness, Point(p.x + loopThickness, p.y))
val endPoint = generateCode(whenExcep.body, w, Point(p.x, areaWhen.end.y))
drawLine(p, Point(p.x + w, p.y))
drawLine(Point(p.x + loopThickness, p.y), Point(p.x, p.y + loopThickness))
drawLine(Point((p.x + w) - loopThickness, p.y), Point(p.x + w, p.y + loopThickness))
endPoint
case module:Module => generate(module.body, w, p)
case _ => p
}
private def generateCode(codeList:List[Node], w:Int, p:Point):Point =
(p /: codeList) { (start:Point, c:Node) => generate(c, w, start) }
}
Nota: no incluyo la clase
LineBreaker, que sirve para cortar un String en varios según sea necesario para que el texto entre en un ancho gráfico determinado.