|
| 1 | +module View.Diagram.FromNLL (fromNLL, ErrDiagramEncoding(..)) where |
| 2 | + |
| 3 | +import Data.Array hiding (head, tail, length) |
| 4 | +import Data.Array.NonEmpty (toNonEmpty) |
| 5 | +import Data.Either |
| 6 | +import Data.Foldable (length) |
| 7 | +import Data.Int (rem) |
| 8 | +import Data.Maybe (Maybe(..)) |
| 9 | +import Data.NonEmpty as NE |
| 10 | +import Data.Vec2D (vec3) |
| 11 | +import Prelude |
| 12 | +import View.Diagram.Model |
| 13 | + |
| 14 | + |
| 15 | +-- Data types --------------------------------------------------------------------------------------------------------- |
| 16 | + |
| 17 | +-- | This represents a directed graph using only its edges. |
| 18 | +-- | Vertices are stored as the source and targets of every edge (or arrow, since it is directed). |
| 19 | +-- | Isolated nodes are represented using identity arrows. |
| 20 | +-- | This allows representing cyclic graphs and multiple disconnected graphs. |
| 21 | +type GraphArrow a = |
| 22 | + { source :: a |
| 23 | + , target :: a |
| 24 | + } |
| 25 | + |
| 26 | +data ErrDiagramEncoding |
| 27 | + = ErrArrayNotRectangular |
| 28 | + | ErrGraphIsCyclic |
| 29 | + |
| 30 | +derive instance eqErrorEncodingDiagram :: Eq ErrDiagramEncoding |
| 31 | + |
| 32 | +instance showErrDiagramEncoding :: Show ErrDiagramEncoding where |
| 33 | + show ErrArrayNotRectangular = "Error: Brick Diagram is not perfectly square" |
| 34 | + show ErrGraphIsCyclic = "Error: Brick Diagram describes a cyclic graph" |
| 35 | + |
| 36 | +type DiagramM a = Either ErrDiagramEncoding a |
| 37 | + |
| 38 | +-- | See https://adoring-curie-7b92fd.netlify.com/. |
| 39 | +type BrickDiagram a = |
| 40 | + { width :: Int |
| 41 | + , elements :: Array a |
| 42 | + } |
| 43 | + |
| 44 | +-- | Makes a brick diagram without checking bounds and sizes. |
| 45 | +mkBrickDiagramUnsafe :: ∀ a. Int -> Array a -> BrickDiagram a |
| 46 | +mkBrickDiagramUnsafe width ops = { width: width, elements: ops } |
| 47 | + |
| 48 | +-- | Safe constructor for brick diagrams, checks that the encoded array has the right size. |
| 49 | +mkBrickDiagram :: ∀ a. Int -> Array a -> DiagramM (BrickDiagram a) |
| 50 | +mkBrickDiagram w ops | length ops `mod` w == 0 = Right $ mkBrickDiagramUnsafe w ops |
| 51 | + | otherwise = Left ErrArrayNotRectangular |
| 52 | + |
| 53 | +-- Brick diagram operations ------------------------------------------------------------------------------------------- |
| 54 | + |
| 55 | +-- | Get the height of a brick diagram, assuming it's perfectly rectangular. |
| 56 | +height :: ∀ a. BrickDiagram a -> Int |
| 57 | +height b = (length b.elements) / b.width |
| 58 | + |
| 59 | +-- | Return the element below the given coordinates, if any. |
| 60 | +below :: ∀ a. BrickDiagram a -> Int -> Int -> Maybe a |
| 61 | +below b x y = let l = min (height b) (y + 1) in |
| 62 | + index b.elements (l * b.width + x) |
| 63 | + |
| 64 | +-- Converting brick diagrams to directed graphs ----------------------------------------------------------------------- |
| 65 | + |
| 66 | +-- | Parses a `BrickDiagram` into a directed graph without identity arrows. |
| 67 | +-- | This ignores all the nodes which are equal to the first argument. |
| 68 | +-- | This also checks that the graph is acyclic. |
| 69 | +parseBrickToGraph :: ∀ a. Eq a => a -> BrickDiagram a -> DiagramM (Array (GraphArrow a)) |
| 70 | +parseBrickToGraph empty = checkAcyclic <<< filter (not isSelfArrow) <<< filterNode empty <<< brickToGraph |
| 71 | + |
| 72 | +-- | Same as `parseBrickToGraph` except the `mempty` nodes are ignored |
| 73 | +parseBrickToGraphMonoid :: ∀ a. Eq a => Monoid a => BrickDiagram a -> DiagramM (Array (GraphArrow a)) |
| 74 | +parseBrickToGraphMonoid = parseBrickToGraph mempty |
| 75 | + |
| 76 | +-- | Given a node a and a directed graph, remove all arrows which have a as source or dest |
| 77 | +filterNode :: ∀ a. Eq a => a -> Array (GraphArrow a) -> Array (GraphArrow a) |
| 78 | +filterNode value = filter (not nodeContains value) |
| 79 | + where |
| 80 | + nodeContains :: a -> GraphArrow a -> Boolean |
| 81 | + nodeContains v a = a.source == v || a.target == v |
| 82 | + |
| 83 | +-- | Checks if an arrow in the graph has the same source and destination. |
| 84 | +isSelfArrow :: ∀ a. Eq a => GraphArrow a -> Boolean |
| 85 | +isSelfArrow {source, target} = source == target |
| 86 | + |
| 87 | +-- | Map a well formed Brick Diagram to a directed graph. |
| 88 | +brickToGraph :: ∀ a. Eq a => BrickDiagram a -> Array (GraphArrow a) |
| 89 | +brickToGraph b = mapMaybe (edge b) indices |
| 90 | + where |
| 91 | + indices = range 0 (length b.elements - 1) |
| 92 | + |
| 93 | +-- | Given a brick diagram and an index, return one of the arrows leaving the node at this index. |
| 94 | +edge :: ∀ a. Eq a => BrickDiagram a -> Int -> Maybe (GraphArrow a) |
| 95 | +edge b i = { source: _, target: _ } <$> index b.elements i <*> below b xPos yPos |
| 96 | + where |
| 97 | + yPos = i / b.width |
| 98 | + xPos = i `rem` b.width |
| 99 | + |
| 100 | +-- Converting Directed Graphs to Diagram ------------------------------------------------------------------------------ |
| 101 | + |
| 102 | +checkAcyclic :: ∀ a. Eq a => Array (GraphArrow a) -> DiagramM (Array (GraphArrow a)) |
| 103 | +checkAcyclic graph = if isAcyclic graph then Right graph else Left ErrGraphIsCyclic |
| 104 | + |
| 105 | +isAcyclic :: ∀ a. Eq a => Array (GraphArrow a) -> Boolean |
| 106 | +isAcyclic graph = case uncons graph of |
| 107 | + Just {head, tail} -> isAcyclicHelper [] head.source graph |
| 108 | + Nothing -> true |
| 109 | + |
| 110 | +isAcyclicHelper :: ∀ a. Eq a => Array a -> a -> Array (GraphArrow a) -> Boolean |
| 111 | +isAcyclicHelper seen current graph = let targets = getTargets current graph in |
| 112 | + all (\t -> (t `not elem` seen) && isAcyclicHelper (t : seen) t graph) targets |
| 113 | + |
| 114 | +-- | Given a node in a graph and the arrows in the graph, return the arrows which have |
| 115 | +-- | the node as source. |
| 116 | +getSources :: ∀ a. Eq a => a -> Array (GraphArrow a) -> Array (GraphArrow a) |
| 117 | +getSources node = filter (\a -> a.source == node) |
| 118 | + |
| 119 | +-- | Given a node and the arrows in the graph, return all the nodes which are direct |
| 120 | +-- | targets of the node. |
| 121 | +getTargets :: ∀ a. Eq a => a -> Array (GraphArrow a) -> Array a |
| 122 | +getTargets node = map _.target <<< getSources node |
| 123 | + |
| 124 | +-- Converting naively from BrickDiagram to DiagramInfo ---------------------------------------------------------------- |
| 125 | + |
| 126 | +brickToDiagram :: ∀ a. Eq a => Show a => BrickDiagram a -> String -> DiagramInfo |
| 127 | +brickToDiagram brick name = { name: name, ops: graphToOps brick } |
| 128 | + |
| 129 | +type ConsecutiveValues a = { value :: a, length :: Int } |
| 130 | + |
| 131 | +-- | This function assumes the brick diagram in argument encodes the information to lay out the diagram, e.g.: |
| 132 | +-- | ``` |
| 133 | +-- | 2 |
| 134 | +-- | 23 |
| 135 | +-- | 11 |
| 136 | +-- | 40 |
| 137 | +-- | ``` |
| 138 | +-- | will display the diagram: |
| 139 | +-- | ``` |
| 140 | +-- | [--2--] [--3--] |
| 141 | +-- | [------1------] |
| 142 | +-- | [--4--] |
| 143 | +-- | ``` |
| 144 | +graphToOps :: ∀ a. Eq a => Show a => BrickDiagram a -> Array Operator |
| 145 | +graphToOps { width: width, elements: brick } = |
| 146 | + let lines = splitLines width brick |
| 147 | + l = mapWithIndex mapOperators $ map packConsecutive lines in |
| 148 | + concat l |
| 149 | + where |
| 150 | + splitLines :: Int -> Array a -> Array (Array a) |
| 151 | + splitLines w array | length array <= w = [array] |
| 152 | + | otherwise = take width array : splitLines width (drop width array) |
| 153 | + |
| 154 | + packConsecutive :: Array a -> Array (ConsecutiveValues a) |
| 155 | + packConsecutive = map ((\n -> { value: NE.head n, length: length n }) <<< toNonEmpty) <<< groupBy (==) |
| 156 | + |
| 157 | + mapOperators :: Int -> Array (ConsecutiveValues a) -> Array Operator |
| 158 | + mapOperators row line = mapWithIndex (mkOperator row) line |
| 159 | + |
| 160 | + mkOperator :: Int -> Int -> ConsecutiveValues a -> Operator |
| 161 | + mkOperator row col value = |
| 162 | + { identifier: show row <> ":" <> show col |
| 163 | + , pos: vec3 row col value.length |
| 164 | + , label: show value.value |
| 165 | + } |
| 166 | + |
| 167 | +-- Converting from NLL ------------------------------------------------------------------------------------------------ |
| 168 | + |
| 169 | +nllToBrickDiagram :: Array Int -> DiagramM (BrickDiagram Int) |
| 170 | +nllToBrickDiagram nll = case uncons nll of |
| 171 | + Just { head, tail } -> mkBrickDiagram head tail |
| 172 | + Nothing -> Right $ mkBrickDiagramUnsafe 0 [] |
| 173 | + |
| 174 | +fromNLL :: Array Int -> String -> DiagramM DiagramInfo |
| 175 | +fromNLL nll name = do bricks <- nllToBrickDiagram nll |
| 176 | + _ <- parseBrickToGraph 0 bricks |
| 177 | + Right $ brickToDiagram bricks name |
0 commit comments