sábado, 4 de abril de 2009

Una vuelta de tuerca más a la generación del diagrama

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.

No hay comentarios:

Publicar un comentario