From 28d3d0544e860e0fed5b7924c834a0500fabaa9a Mon Sep 17 00:00:00 2001 From: Hans-Peter Deifel <hpd@hpdeifel.de> Date: Sun, 9 Dec 2018 20:42:40 +0100 Subject: [PATCH] web: Implement graph view --- web/assets/custom.css | 14 +++++ web/assets/index.html | 41 +++++++++++++ web/backend/Main.hs | 46 +++++++++++++++ web/backend/copar-web.cabal | 1 + web/frontend/src/Main.elm | 115 +++++++++++++++++++++++++----------- 5 files changed, 184 insertions(+), 33 deletions(-) diff --git a/web/assets/custom.css b/web/assets/custom.css index 44b6bce..800934d 100644 --- a/web/assets/custom.css +++ b/web/assets/custom.css @@ -6,6 +6,10 @@ font-weight: 300; } +.body { + height: 100%; +} + .header { background-color: rgb(0, 120, 231); padding-top: 1em; @@ -21,6 +25,7 @@ .output { padding: 1em; + height: 100%; } .error-message { @@ -47,3 +52,12 @@ padding: 0px; list-style: none; } + +#graph { + display: block; + height: 400px; +} + +#source { + height: 400px; +} diff --git a/web/assets/index.html b/web/assets/index.html index 545171b..cb68d0e 100644 --- a/web/assets/index.html +++ b/web/assets/index.html @@ -3,6 +3,9 @@ <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> + <script type="text/javascript" src="https://unpkg.com/cytoscape@3.3.0/dist/cytoscape.min.js"></script> + <script src="https://unpkg.com/dagre@0.7.4/dist/dagre.js"></script> + <script type="text/javascript" src="https://unpkg.com/cytoscape-dagre@2.2.2/cytoscape-dagre.js"></script> <script src="main.js"></script> <link rel="stylesheet" href="https://unpkg.com/purecss@1.0.0/build/pure-min.css" integrity="sha384-nn4HPE8lTHyVtfCBi5yW9d20FjT8BJwUXyWZT9InLYax14RDjBj46LmSztkmNP9w" crossorigin="anonymous"> <link rel="stylesheet" href="https://unpkg.com/purecss@1.0.0/build/grids-responsive-min.css"> @@ -15,6 +18,44 @@ var app = Elm.Main.init({ node: document.getElementById('elm') }); + app.ports.drawGraph.subscribe(function(data) { + requestAnimationFrame(function() { + + console.log(data); + + cytoscape({ + container: document.getElementById('graph'), + elements: data, + style: [ + { + selector: 'edge', + style: { + 'curve-style': 'bezier', + 'target-arrow-shape': 'triangle', + 'control-point-step-size': '70px', + 'width': '3' + } + }, + { + selector: '[label]', + style: { + 'label': 'data(label)', + 'text-valign': 'center', + 'background-color': 'rgb(0, 120, 231)', + 'color': 'white', + 'width': 'label', + 'height': 'label', + 'padding-relative-to': 'max', + 'padding': '2em' + } + } + ], + layout: { + name: 'dagre', + } + }) + }) + }) </script> </body> </html> diff --git a/web/backend/Main.hs b/web/backend/Main.hs index a459742..1590e37 100644 --- a/web/backend/Main.hs +++ b/web/backend/Main.hs @@ -8,6 +8,7 @@ import System.Environment import System.IO import Data.Function ( (&) ) import Data.Maybe (fromJust) +import Data.Bifunctor import Web.Scotty import qualified Data.Text.Lazy.Encoding as TL @@ -26,6 +27,8 @@ import Network.Wai.Handler.Warp ( setPort import Data.Aeson ((.=)) import qualified Data.Aeson as Json import qualified Data.HashMap.Strict as HM +import Data.Vector (Vector) +import qualified Data.Vector as V import qualified Copar.Parser as P import Copar.Algorithm @@ -33,6 +36,7 @@ import Copar.PartitionPrinter import qualified Data.Partition as Partition import Data.Partition (Partition, State) import Data.MorphismEncoding (Encoding) +import qualified Data.MorphismEncoding as Encoding import Copar.Coalgebra.Parser (SymbolTable, fromSymbolTable) data Reply = Error Text | Result [[State]] SymbolTable @@ -56,6 +60,44 @@ doRefine input = case P.parseCoalgebra P.defaultConfig "input" input of in Result (restrictPartitionToSort1 encoding partition) symbolTable +data GraphReply = GraphError Text | Graph (Encoding () ()) SymbolTable + +instance Json.ToJSON GraphReply where + toJSON (GraphError e) = + Json.object ["type" .= ("error" :: Text), "content" .= e] + toJSON (Graph encoding symbolTable) = + Json.object [ "type" .= ("graph" :: Text) + , "content" .= encodingToJson encoding symbolTable + ] + +encodingToJson :: Encoding a b -> SymbolTable -> Json.Value +encodingToJson encoding symbolTable = + Json.Array $ + V.map wrapData + (V.imap encodeState (Encoding.structure encoding) + <> V.imap encodeEdges (Encoding.edges encoding)) + + where + wrapData x = Json.object ["data" .= x] + encodeState i _ = + Json.object (("id" .= ("s" ++ show i)):label i) + encodeEdges i (Encoding.Edge from _ to) = + Json.object [ "id" .= ("e" ++ show i) + , "source" .= state from + , "target" .= state to + ] + state = ("s" ++) . show + label i = case HM.lookup i (fromSymbolTable symbolTable) of + Just name -> [ "label" .= name] + Nothing -> [] + +doGraph :: Text -> GraphReply +doGraph input = case P.parseCoalgebra P.defaultConfig "input" input of + Left e -> GraphError (T.pack e) + Right (f, (symbolTable, encoding)) -> + Graph (bimap (const ()) (const ()) encoding) symbolTable + + main :: IO () main = getArgs >>= \case [path] -> do @@ -74,6 +116,10 @@ main = getArgs >>= \case post "/refine" $ do input <- TL.decodeUtf8 <$> body json . doRefine . TL.toStrict $ input + + post "/graph" $ do + input <- TL.decodeUtf8 <$> body + json . doGraph . TL.toStrict $ input _ -> do self <- getProgName hPutStrLn stderr $ "Usage: " ++ self ++ " ASSETS_PATH" diff --git a/web/backend/copar-web.cabal b/web/backend/copar-web.cabal index db5e8de..292d552 100644 --- a/web/backend/copar-web.cabal +++ b/web/backend/copar-web.cabal @@ -19,3 +19,4 @@ executable copar-web , warp >= 3.2.22 && <3.3 , aeson >= 1.4.2 && <1.5 , unordered-containers >= 0.2.9 && < 0.3 + , vector >= 0.12.0 && <0.13 diff --git a/web/frontend/src/Main.elm b/web/frontend/src/Main.elm index 0a72ea1..b3ec818 100644 --- a/web/frontend/src/Main.elm +++ b/web/frontend/src/Main.elm @@ -1,11 +1,12 @@ -module Main exposing (main) +port module Main exposing (main) import Browser import Html exposing (Html) import Html.Attributes as Attr import Html.Events as Html import Http -import Json.Decode as Json +import Json.Decode as D +import Json.Encode as E -- MAIN @@ -18,6 +19,7 @@ main = , view = view } +port drawGraph : E.Value -> Cmd msg -- MODEL @@ -30,8 +32,10 @@ type alias Model = type OutputStatus = NothingYet - | WaitingForResult - | Result (Result String Blocks) + | WaitingForPartition + | PartitionResult (Result String Blocks) + | WaitingForGraph + | GraphResult (Result String ()) init : () -> (Model, Cmd Msg) @@ -49,7 +53,9 @@ type alias Blocks = List (List String) type Msg = ChangeInput String | Refine - | GotResult (Result Error Blocks) + | GotPartition (Result Error Blocks) + | Graph + | GotGraph (Result Error D.Value) update : Msg -> Model -> (Model, Cmd Msg) @@ -58,15 +64,32 @@ update msg model = ChangeInput newInputText -> ({ model | inputText = newInputText }, Cmd.none) Refine -> - ( { model | outputStatus = WaitingForResult } + ( { model | outputStatus = WaitingForPartition } , refine model.inputText ) - GotResult result -> + Graph -> + ( { model | outputStatus = WaitingForGraph } + , graph model.inputText + ) + GotPartition result -> + case result of + Ok txt -> ( {model | outputStatus = PartitionResult (Ok txt) }, Cmd.none ) + Err (HttpError err) -> + ( { model | outputStatus = PartitionResult (Err ("Request to server failed with " + ++ showHttpError err)) } + , Cmd.none + ) + Err (ServerError err) -> ( { model | outputStatus = PartitionResult (Err err) }, Cmd.none ) + GotGraph result -> case result of - Ok txt -> ( {model | outputStatus = Result (Ok txt) }, Cmd.none ) - Err (HttpError err) -> ( { model | outputStatus = Result (Err ("Request to server failed with " ++ showHttpError err)) }, Cmd.none ) - Err (ServerError err) -> ( { model | outputStatus = Result (Err err) }, Cmd.none ) - + Ok g -> ({ model | outputStatus = GraphResult (Ok ())}, drawGraph g) + Err (HttpError err) -> + ( { model | outputStatus = GraphResult (Err ("Request to server failed with " + ++ showHttpError err))} + , Cmd.none + ) + Err (ServerError err) -> ( { model | outputStatus = GraphResult (Err err) }, Cmd.none ) + showHttpError : Http.Error -> String showHttpError err = case err of @@ -80,30 +103,47 @@ showHttpError err = refine : String -> Cmd Msg refine input = Http.post - { + { url = "refine" , body = Http.stringBody "text/plain" input - , expect = Http.expectJson (GotResult << mkResult) resultDecoder + , expect = Http.expectJson (GotPartition << mkResult) resultDecoder } -mkResult : Result Http.Error (Result String Blocks) -> Result Error Blocks +mkResult : Result Http.Error (Result String a) -> Result Error a mkResult res = case res of (Ok (Ok s)) -> Ok s (Ok (Err s)) -> Err (ServerError s) (Err e) -> Err (HttpError e) - -resultDecoder : Json.Decoder (Result String Blocks) -resultDecoder = Json.field "type" Json.string |> Json.andThen resultDecoderImpl - -resultDecoderImpl : String -> Json.Decoder (Result String Blocks) + +resultDecoder : D.Decoder (Result String Blocks) +resultDecoder = D.field "type" D.string |> D.andThen resultDecoderImpl + +resultDecoderImpl : String -> D.Decoder (Result String Blocks) resultDecoderImpl typ = case typ of - "error" -> Json.map Err (Json.field "content" Json.string) - "result" -> Json.map Ok (Json.field "content" (Json.list (Json.list Json.string))) - other -> Json.fail ("Unknown server response of type " ++ other) + "error" -> D.map Err (D.field "content" D.string) + "result" -> D.map Ok (D.field "content" (D.list (D.list D.string))) + other -> D.fail ("Unknown server response of type " ++ other) - + +graph : String -> Cmd Msg +graph input = + Http.post + { url = "graph" + , body = Http.stringBody "text/plain" input + , expect = Http.expectJson (GotGraph << mkResult) graphDecoder + } + +graphDecoder : D.Decoder (Result String D.Value) +graphDecoder = D.field "type" D.string |> D.andThen graphDecoderImpl + +graphDecoderImpl : String -> D.Decoder (Result String D.Value) +graphDecoderImpl typ = + case typ of + "error" -> D.map Err (D.field "content" D.string) + "graph" -> D.map Ok (D.field "content" D.value) + other -> D.fail ("Unknown server response of type " ++ other) -- SUBSCRIPTIONS @@ -117,14 +157,14 @@ subscriptions model = view : Model -> Html Msg view model = - Html.div [] + Html.div [Attr.class "body"] [ Html.div [Attr.class "header"] [Html.h1 [] [ Html.text "CoPaR" ]] , Html.div [Attr.class "pure-g"] [ Html.div [Attr.class "pure-u-1 pure-u-md-1-2"] [viewInput model] , Html.div [Attr.class "pure-u-1 pure-u-md-1-2"] [viewOutput model] ] ] - + viewInput : Model -> Html Msg viewInput model = Html.div [Attr.class "input"] @@ -134,14 +174,20 @@ viewInput model = , Attr.rows 25 , Attr.placeholder "Coalgebra specificiation" , Attr.value model.inputText - , Html.onInput ChangeInput + , Html.onInput ChangeInput , Attr.class "pure-input-1" + , Attr.id "source" ] [] - , Html.button [ if model.outputStatus == WaitingForResult + , Html.button [ if model.outputStatus == WaitingForPartition -- todo all waiting then Attr.disabled True else Html.onClick Refine - , Attr.class "pure-button pure-button-primary pure-input-1" + , Attr.class "pure-button pure-button-primary pure-input-1-2" ] [ Html.text "REFINE" ] + , Html.button [ if model.outputStatus == WaitingForGraph -- todo all waiting + then Attr.disabled True + else Html.onClick Graph + , Attr.class "pure-button pure-button-primary pure-input-1-2" + ] [ Html.text "GRAPH" ] ] ] @@ -151,14 +197,17 @@ viewOutput model = [ Html.h2 [] [ Html.text "Result"] , case model.outputStatus of NothingYet -> Html.div [] [] - WaitingForResult -> Html.text "waiting..." - Result (Ok res) -> - Html.div [] [ Html.h4 [] [Html.text "Blocks of Resulting Partition:"] + WaitingForPartition -> Html.text "waiting..." + PartitionResult (Ok res) -> + Html.div [Attr.id "blocks"] [ Html.h4 [] [Html.text "Blocks of Resulting Partition:"] , viewBlocks res ] - Result (Err res) -> Html.pre [Attr.class "error-message"] [Html.text res] + PartitionResult (Err res) -> Html.pre [Attr.class "error-message"] [Html.text res] + WaitingForGraph -> Html.text "waiting..." + GraphResult (Ok g) -> Html.div [Attr.id "graph"] [] + GraphResult (Err res) -> Html.pre [Attr.class "error-message"] [Html.text res] ] - + viewBlocks : Blocks -> Html Msg viewBlocks blocks = blocks -- GitLab