All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.mdr.ascii.layout.coordAssign.Layouter.scala Maven / Gradle / Ivy

The newest version!
package com.github.mdr.ascii.layout.coordAssign

import com.github.mdr.ascii.layout.drawing._
import com.github.mdr.ascii.layout.layering._
import com.github.mdr.ascii.common._
import com.github.mdr.ascii.util.Utils
import com.github.mdr.ascii.util.Utils._

object Layouter {

  private val MINIMUM_VERTEX_HEIGHT = 3

}

class Layouter(vertexRenderingStrategy: VertexRenderingStrategy[_], vertical: Boolean = true) {

  import Layouter._

  case class LayoutState(previousLayerInfo: LayerInfo, incompleteEdges: Map[DummyVertex, List[Point]], drawingElements: List[DrawingElement]) {

    def mergeLayerResult(result: LayerLayoutResult): LayoutState = {
      val LayerLayoutResult(elements, updatedLayerInfo, updatedIncompletedEdges) = result
      LayoutState(updatedLayerInfo, updatedIncompletedEdges, drawingElements ++ elements)
    }

  }

  def layout(layering: Layering): Drawing = {
    val layerInfos: Map[Layer, LayerInfo] = calculateLayerInfos(layering)

    var layoutState = LayoutState(LayerInfo(Map()), Map(), Nil)
    for (layer ← layering.layers) {
      val layerResult = layoutLayer(layoutState.previousLayerInfo, layerInfos(layer), layering.edges, layoutState.incompleteEdges)
      layoutState = layoutState.mergeLayerResult(layerResult)
    }

    Drawing(layoutState.drawingElements)
  }

  /**
   * Calculate layer infos, with vertices given their correct column coordinates, but awaiting
   * correct row coordinates.
   */
  private def calculateLayerInfos(layering: Layering): Map[Layer, LayerInfo] = {
    var layerInfos: Map[Layer, LayerInfo] = Map()
    for ((previousLayerOpt, currentLayer, nextLayerOpt) ← Utils.withPreviousAndNext(layering.layers)) {
      val layerInfo = calculateLayerInfo(currentLayer, layering.edges, previousLayerOpt, nextLayerOpt)
      layerInfos += currentLayer -> layerInfo
    }
    PortNudger.nudge(layering, spaceVertices(layerInfos))
  }

  /**
   * For each vertex in the layer, calculate its size and assign ports for its in- and out-edges.
   */
  private def calculateLayerInfo(layer: Layer, edges: List[Edge], previousLayerOpt: Option[Layer], nextLayerOpt: Option[Layer]): LayerInfo = {
    val inEdges = previousLayerOpt.map { previousLayer ⇒
      edges.sortBy { case Edge(v1, _) ⇒ previousLayer.vertices.indexOf(v1) }
    }.getOrElse(Nil)
    val outEdges = nextLayerOpt.map { nextLayer ⇒
      edges.sortBy { case Edge(_, v2) ⇒ nextLayer.vertices.indexOf(v2) }
    }.getOrElse(Nil)
    def getInEdges(vertex: Vertex) = inEdges collect { case e @ Edge(v1, `vertex`) ⇒ e }
    def getOutEdges(vertex: Vertex) = outEdges collect { case e @ Edge(`vertex`, v2) ⇒ e }

    def getDimension(vertex: Vertex): Dimension = vertex match {
      case v: RealVertex  ⇒ calculateVertexDimension(v, getInEdges(vertex).size, getOutEdges(vertex).size)
      case _: DummyVertex ⇒ Dimension(height = 1, width = 1)
    }
    val dimensions: Map[Vertex, Dimension] = makeMap(layer.vertices, getDimension)
    val regions: Map[Vertex, (Region, Region)] = calculateVertexRegions(layer, dimensions)
    def buildVertexInfo(v: Vertex) = {
      val (boxRegion, greaterRegion) = regions(v)
      makeVertexInfo(v, boxRegion, greaterRegion, getInEdges(v), getOutEdges(v))
    }
    LayerInfo(makeMap(layer.vertices, buildVertexInfo))
  }

  private def makeVertexInfo(vertex: Vertex, boxRegion: Region, greaterRegion: Region, inEdges: List[Edge], outEdges: List[Edge]): VertexInfo =
    vertex match {
      case realVertex: RealVertex   ⇒ makeVertexInfo(realVertex, boxRegion, greaterRegion, inEdges, outEdges)
      case dummyVertex: DummyVertex ⇒ makeVertexInfo(dummyVertex, boxRegion, greaterRegion, inEdges, outEdges)
    }

  private def makeVertexInfo(vertex: RealVertex, boxRegion: Region, greaterRegion: Region, inEdges: List[Edge], outEdges: List[Edge]): VertexInfo = {
    val inDegree = inEdges.size + vertex.selfEdges
    val inPorts: List[Point] = portOffsets(inDegree, boxRegion.width).map(boxRegion.topLeft.right)
    val inEdgeToPortMap = inEdges.zip(inPorts).toMap
    val selfInPorts = inPorts.drop(inEdges.size)

    val outDegree = outEdges.size + vertex.selfEdges
    val outPorts = portOffsets(outDegree, boxRegion.width).map(boxRegion.bottomLeft.right)
    val outEdgeToPortMap = outEdges.zip(outPorts).toMap
    val selfOutPorts = outPorts.drop(outEdges.size)

    VertexInfo(boxRegion, greaterRegion, inEdgeToPortMap, outEdgeToPortMap, selfInPorts, selfOutPorts)
  }

  private def makeVertexInfo(vertex: DummyVertex, boxRegion: Region, greaterRegion: Region, inEdges: List[Edge], outEdges: List[Edge]): VertexInfo = {
    val (List(inVertex), List(outVertex)) = (inEdges, outEdges)
    val port = boxRegion.topLeft
    val inEdgeToPortMap = Map(inVertex -> port)
    val outEdgeToPortMap = Map(outVertex -> port)
    VertexInfo(boxRegion, greaterRegion, inEdgeToPortMap, outEdgeToPortMap, Nil, Nil)
  }

  /**
   * Space out edge ports even along the edge of a vertex.
   *
   * We leave room for self edges at the right
   */
  private def portOffsets(edges: List[Edge], vertexWidth: Int, selfEdges: Int): Map[Edge, Int] = {
    val factor = vertexWidth / (edges.size + selfEdges + 1)
    val centraliser = (vertexWidth - factor * (edges.size + selfEdges + 1)) / 2
    edges.zipWithIndex.map { case (v, i) ⇒ (v, (i + 1) * factor + centraliser) }.toMap
  }

  /**
   * Space out edge ports evenly along the top or bottom edge of a vertex.
   */
  private def portOffsets(portCount: Int, vertexWidth: Int): List[Int] = {
    val factor = vertexWidth / (portCount + 1)
    val centraliser = (vertexWidth - factor * (portCount + 1)) / 2
    0.until(portCount).toList.map(i ⇒ (i + 1) * factor + centraliser)
  }

  /**
   * Calculate dimension based on vertex rendering strategy together with the number of in/out edges
   */
  private def calculateVertexDimension(v: RealVertex, inDegree: Int, outDegree: Int) = {
    val selfEdges = v.selfEdges
    def requiredWidth(degree: Int) =
      if (vertical)
        (degree + selfEdges) * 2 + 1 + 2
      else {
        // We could draw horizontal diagrams more compactly , but the PortNudger requires 
        // space at the moment.
        // degree + selfEdges + 2  
        (degree + selfEdges) * 2 + 1 + 2
      }
    val requiredInputWidth = requiredWidth(inDegree)
    val requiredOutputWidth = requiredWidth(outDegree)
    val Dimension(preferredHeight, preferredWidth) = getPreferredSize(vertexRenderingStrategy, v)
    val width = math.max(math.max(requiredInputWidth, requiredOutputWidth), preferredWidth + 2)
    val height = math.max(MINIMUM_VERTEX_HEIGHT, preferredHeight + 2)
    Dimension(height = height, width = width)
  }

  /**
   * Initially pack vertex regions close together, so we can determine the minimum width of the entire
   * drawing.
   *
   * @return pair of regions: first = region of the vertex box; second = "great region" which includes the space for any
   *   self edges which need to wrap around the vertex.
   */
  private def calculateVertexRegions(layer: Layer, dimensions: Map[Vertex, Dimension]): Map[Vertex, (Region, Region)] = {
    var regions: Map[Vertex, (Region, Region)] = Map()
    var nextVertexTopLeft = Point(0, 0)
    for (vertex ← layer.vertices) {
      val boxRegion = Region(nextVertexTopLeft, dimensions(vertex))
      // Spacing to the right of the vertex for self edges to wrap aroud:
      val selfEdgesSpacing = vertex match {
        case realVertex: RealVertex if realVertex.selfEdges > 0 ⇒ realVertex.selfEdges * 2
        case _                                                  ⇒ 0
      }
      val greaterRegion = boxRegion.expandRight(selfEdgesSpacing).expandUp(selfEdgesSpacing).expandDown(selfEdgesSpacing)
      regions += vertex -> (boxRegion, greaterRegion)
      nextVertexTopLeft = boxRegion.topRight.right(selfEdgesSpacing + 2)
    }
    regions
  }

  /**
   * Calculate the width of the diagram (assuming the widest row is packed together as closely as possible)
   */
  private def calculateDiagramWidth(layerInfos: Map[Layer, LayerInfo]) = {
    def vertexWidth(vertexInfo: VertexInfo) = vertexInfo.greaterRegion.width
    def layerWidth(layerInfo: LayerInfo) = {
      val vertexInfos = layerInfo.vertexInfos.values
      val spacing = vertexInfos.size
      vertexInfos.map(vertexWidth).sum + spacing - 1
    }
    layerInfos.values.map(layerWidth).fold(0)(_ max _)
  }

  private def spaceVertices(layerInfos: Map[Layer, LayerInfo]): Map[Layer, LayerInfo] = {
    val diagramWidth = calculateDiagramWidth(layerInfos)
    layerInfos.map { case (layer, info) ⇒ layer -> spaceVertices(layer, info, diagramWidth) }
  }

  /**
   * Space out vertices horizontally across the full width of the diagram, and centre them vertically within
   * the layer.
   */
  private def spaceVertices(layer: Layer, layerVertexInfos: LayerInfo, diagramWidth: Int): LayerInfo = {
    val excessSpace = diagramWidth - layerVertexInfos.maxColumn
    val horizontalSpacing = math.max(excessSpace / (layerVertexInfos.vertexInfos.size + 1), 1)

    // Height of the vertices in the layer (excluding self edges)
    val layerHeight = layerVertexInfos.vertexInfos.values.map(_.boxRegion.height).max

    var leftColumn = horizontalSpacing
    val newVertexInfos =
      for {
        v ← layer.vertices
        vertexInfo ← layerVertexInfos.vertexInfo(v)
      } yield {
        val oldLeftColumn = leftColumn
        leftColumn += vertexInfo.greaterRegion.width
        leftColumn += horizontalSpacing
        val verticalCenteringOffset = (layerHeight - vertexInfo.boxRegion.height) / 2
        v -> vertexInfo.setLeft(oldLeftColumn).down(verticalCenteringOffset)
      }
    LayerInfo(newVertexInfos.toMap)
  }

  private case class LayerLayoutResult(
    drawingElements: List[DrawingElement],
    layerInfo: LayerInfo,
    updatedIncompletedEdges: Map[DummyVertex, List[Point]])

  /**
   * 1) Decide the (vertical) order of edges coming into the currentLayer -- that is, what row they bend on (if required)
   * 2) Decide the vertical position of the vertices in the currentLayer.
   * 3) Render the incoming edges and current layer vertices into diagram elements.
   * 4) Update bookkeeping information about as-yet-incomplete edges.
   *
   * @param edges -- edges from the previous layer into the current layer
   * @param incompleteEdges -- map from a dummy vertex in previous layer (which is the bottom-most tip of an incomplete
   *                           long edge), to the sequence of points that make up the edge built so far.
   */
  private def layoutLayer(
    previousLayerInfo: LayerInfo,
    currentLayerInfo: LayerInfo,
    edges: List[Edge],
    incompleteEdges: Map[DummyVertex, List[Point]]): LayerLayoutResult = {

    val edgeInfos: List[EdgeInfo] = makeEdgeInfos(edges, previousLayerInfo, currentLayerInfo)

    val edgeZoneTopRow = if (previousLayerInfo.isEmpty) -1 /* first layer */ else previousLayerInfo.maxRow + 1
    val edgeBendCalculator = new EdgeBendCalculator(edgeInfos, edgeZoneTopRow, currentLayerInfo.topSelfEdgeBuffer)

    val edgeInfoToPoints: Map[EdgeInfo, List[Point]] =
      makeMap(edgeInfos, edgeInfo ⇒ getEdgePoints(edgeInfo, edgeBendCalculator, incompleteEdges))

    val updatedIncompleteEdges: Map[DummyVertex, List[Point]] =
      for ((EdgeInfo(_, finishVertex: DummyVertex, _, _, _), points) ← edgeInfoToPoints)
        yield finishVertex -> points

    val updatedLayerInfo = currentLayerInfo.down(edgeBendCalculator.edgeZoneBottomRow + 1)

    val vertexElements = makeVertexElements(updatedLayerInfo)
    val edgeElements = makeEdgeElements(edgeInfoToPoints)
    val selfEdgeElements = updatedLayerInfo.vertexInfos.collect {
      case (realVertex: RealVertex, vertexInfo) ⇒
        val boxRightEdge = vertexInfo.boxRegion.rightColumn
        vertexInfo.selfOutPorts.zip(vertexInfo.selfInPorts).reverse.zipWithIndex map {
          case ((out, in), i) ⇒
            val p1 = out.down(1)
            val p2 = p1.down(i + 1)
            val p3 = p2.right(boxRightEdge - p2.column + i * 2 + 2)
            val p4 = p3.up(vertexInfo.boxRegion.height + 2 * (i + 1) + 1)
            val p5 = p4.left(p4.column - in.column)
            val p6 = in.up(1)
            EdgeDrawingElement(List(p1, p2, p3, p4, p5, p6), false, true)
        }
    }.toList.flatten
    LayerLayoutResult(vertexElements ++ edgeElements ++ selfEdgeElements, updatedLayerInfo, updatedIncompleteEdges)
  }

  private def makeEdgeInfos(edges: List[Edge], previousLayerInfo: LayerInfo, currentLayerInfo: LayerInfo): List[EdgeInfo] =
    for {
      edge @ Edge(v1, v2) ← edges
      previousVertexInfo ← previousLayerInfo.vertexInfo(v1)
      currentVertexInfo ← currentLayerInfo.vertexInfo(v2)
      start = previousVertexInfo.outEdgeToPortMap(edge).down
      finish = currentVertexInfo.inEdgeToPortMap(edge).up // Note that this will be at the wrong row, we'll adjust later
    } yield EdgeInfo(v1, v2, start, finish, edge.reversed)

  private def getEdgePoints(edgeInfo: EdgeInfo, edgeBendCalculator: EdgeBendCalculator, incompleteEdges: Map[DummyVertex, List[Point]]): List[Point] = {
    val EdgeInfo(startVertex, _, start, finish, _) = edgeInfo
    val trueFinish = finish.translate(down = edgeBendCalculator.edgeZoneBottomRow + 1)
    val priorPoints: List[Point] = startVertex match {
      case dv: DummyVertex ⇒ incompleteEdges(dv)
      case _: RealVertex   ⇒ List(start)
    }
    val lastPriorPoint = priorPoints.last
    val edgePoints =
      if (lastPriorPoint.column == trueFinish.column) // No bend required
        priorPoints :+ trueFinish
      else {
        require(edgeInfo.requiresBend, edgeInfo + ", " + priorPoints)
        val bendRow = edgeBendCalculator.bendRow(edgeInfo)
        priorPoints ++ List(lastPriorPoint.withRow(bendRow), trueFinish.withRow(bendRow), trueFinish)
      }
    Point.removeRedundantPoints(edgePoints)
  }

  private def makeEdgeElements(edgeInfoToPoints: Map[EdgeInfo, List[Point]]): List[EdgeDrawingElement] =
    for ((EdgeInfo(_, finishVertex: RealVertex, _, _, reversed), points) ← edgeInfoToPoints.toList)
      yield EdgeDrawingElement(points, reversed, !reversed)

  private def makeSelfEdgeElements(layerInfo: LayerInfo): List[EdgeDrawingElement] =
    layerInfo.vertexInfos.collect {
      case (realVertex: RealVertex, vertexInfo) ⇒
        vertexInfo.selfOutPorts.zip(vertexInfo.selfInPorts).reverse.zipWithIndex map {
          case ((out, in), i) ⇒ makeSelfEdgeElement(vertexInfo, out, in, i)
        }
    }.toList.flatten

  /**
   * Trace out a self loop path around the right side of the box:
   *
   * p5 ╭───╮p4
   * p6 v   │
   *  ╭───╮ │
   *  │ A │ │
   *  ╰─┬─╯ │
   * p1 │   │
   * p2 ╰───╯p3
   *
   * @param selfEdgeIndex -- if there are multiple self edges, they are drawn nested inside. The edges are numbered
   *  starting with those anchored on the rightmost ports, proceeding leftwards as the index increases.
   */
  private def makeSelfEdgeElement(vertexInfo: VertexInfo, outPort: Point, inPort: Point, selfEdgeIndex: Int): EdgeDrawingElement = {
    val boxRightEdge = vertexInfo.boxRegion.rightColumn
    val p1 = outPort.down(1)
    val p2 = p1.down(selfEdgeIndex + 1)
    val p3 = p2.right(boxRightEdge - p2.column + selfEdgeIndex * 2 + 2)
    val p4 = p3.up(vertexInfo.boxRegion.height + 2 * (selfEdgeIndex + 1) + 1)
    val p5 = p4.left(p4.column - inPort.column)
    val p6 = inPort.up(1)
    EdgeDrawingElement(List(p1, p2, p3, p4, p5, p6), false, true)
  }

  private def makeVertexElements(layerInfo: LayerInfo): List[VertexDrawingElement] =
    layerInfo.realVertexInfos.map {
      case (realVertex, info) ⇒
        val text = getText(vertexRenderingStrategy, realVertex, info.contentRegion.dimension)
        VertexDrawingElement(info.boxRegion, text)
    }

  private def getPreferredSize[V](vertexRenderingStrategy: VertexRenderingStrategy[V], realVertex: RealVertex): Dimension = {
    val preferredSize = vertexRenderingStrategy.getPreferredSize(realVertex.contents.asInstanceOf[V])
    if (vertical)
      preferredSize
    else
      preferredSize.transpose
  }

  private def getText[V](vertexRenderingStrategy: VertexRenderingStrategy[V], realVertex: RealVertex, preferredSize: Dimension) = {
    val actualPreferredSize = if (vertical) preferredSize else preferredSize.transpose
    vertexRenderingStrategy.getText(realVertex.contents.asInstanceOf[V], actualPreferredSize)
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy