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