packages feed

birch-beer-0.2.3.0: src/BirchBeer/Interactive.hs

{- BirchBeer.Interactive
Gregory W. Schwartz

Interactive version of the tree.
-}

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE RankNTypes #-}

module BirchBeer.Interactive
    ( interactiveDiagram
    ) where

-- Remote
import Data.Bool (bool)
import Data.Colour.SRGB (sRGB24reads)
import Data.Maybe (catMaybes, fromMaybe)
import Data.Tree (Tree (..))
import Safe (headMay)
import Text.Read (readMaybe)
import System.IO.Temp (withTempFile)
import qualified Control.Lens as L
import qualified Data.Clustering.Hierarchical as HC
import qualified Data.Set as Set
import qualified Data.Text as T
import qualified Data.Vector as V
import qualified Diagrams.Backend.Cairo as D
import qualified Diagrams.Backend.Gtk as D
import qualified Diagrams.Prelude as D
import qualified Graphics.UI.Gtk as Gtk
import qualified System.IO as IO
import qualified Typed.Spreadsheet as TS

-- Local
import BirchBeer.MainDiagram
import BirchBeer.Types

-- | Interactive version of the tree.
interactiveDiagram
    :: (Eq a, Ord a, TreeItem a, MatrixLike b)
    => Tree (TreeNode (V.Vector a))
    -> Maybe LabelMap
    -> Maybe b
    -> Maybe (SimMatrix b)
    -> IO ()
interactiveDiagram tree labelMap mat simMat = graphicalUI' "birch-beer" $ do
    minSize'  <-
        fmap (MinClusterSize . round) $ TS.spinButtonAt 1 "Minimum cluster size" 1
    maxStep'  <- fmap (MaxStep . round)
               $ TS.spinButtonAt 1000 "Maximum number of steps from root" 1
    order'    <- fmap Order $ TS.spinButtonAt 1 "Order of diversity" 1
    drawLeafTemp <- TS.radioButton
                        "Leaf type"
                        DrawText
                        [ DrawItem DrawLabel
                        , DrawItem DrawSumContinuous
                        , DrawItem (DrawContinuous ["GENE"])
                        , DrawItem DrawDiversity
                        ]
    drawContinuousGene' <- TS.entry "[GENE] for DrawItem DrawContinuous"
    drawCollection' <-
        TS.radioButton "Leaf shape" PieChart [PieRing, PieNone, CollectionGraph 1 0 []]
    drawMark' <- TS.radioButton "Node mark" MarkNone [MarkModularity]
    drawPalette' <- TS.radioButton "Palette" Set1 [Hsv, Ryb]
    drawNodeNumber' <- fmap DrawNodeNumber $ TS.checkBox "Show node number"
    drawMaxNodeSize' <-
        fmap DrawMaxNodeSize
            $ TS.spinButtonAt 72 "Maximum size of drawn node" 1
    drawMaxLeafNodeSize' <-
        fmap DrawMaxLeafNodeSize
            $ TS.spinButtonAt 72 "Maximum size of drawn leaf node" 1
    drawNoScaleNodes' <-
        fmap DrawNoScaleNodesFlag $ TS.checkBox "Do not scale nodes"
    drawLegendSep' <-
        fmap DrawLegendSep
            $ TS.spinButtonAt 1 "The amount of space between legend and tree" 1
    drawColors' <- fmap
                    (\ x
                    -> ( (\xs -> if null xs then Nothing else Just $ CustomColors xs)
                         . catMaybes
                         . fmap (fmap fst . headMay . sRGB24reads)
                       )
                   =<< ( (\x -> readMaybe x :: Maybe [String])
                       . T.unpack
                       $ x
                       )
                    )
                 $ TS.entryAt "[]" "Custom node colors [\"#e41a1c\", \"#377eb8\"]"
    drawScaleSaturation' <-
        fmap (Just . DrawScaleSaturation)
            $ TS.spinButtonAt 1 "Saturate colors in the HSV model" 1

    return $
        let drawLeaf' = case drawLeafTemp of
                            DrawItem (DrawContinuous _) ->
                                DrawItem (DrawContinuous (fromMaybe [] $ readMaybe (T.unpack drawContinuousGene') :: [T.Text]))
                            x -> x
            config = Config { _birchLabelMap            = labelMap
                            , _birchMinSize             = Just minSize'
                            , _birchMaxStep             = Just maxStep'
                            , _birchMaxProportion       = Nothing
                            , _birchMinDistance         = Nothing
                            , _birchMinDistanceSearch   = Nothing
                            , _birchSmartCutoff         = Nothing
                            , _birchElbowCutoff         = Nothing
                            , _birchCustomCut           = CustomCut Set.empty
                            , _birchRootCut             = Nothing
                            , _birchOrder               = Nothing
                            , _birchDrawLeaf            = drawLeaf'
                            , _birchDrawCollection      = drawCollection'
                            , _birchDrawMark            = drawMark'
                            , _birchDrawNodeNumber      = drawNodeNumber'
                            , _birchDrawMaxNodeSize     = drawMaxNodeSize'
                            , _birchDrawMaxLeafNodeSize = drawMaxLeafNodeSize'
                            , _birchDrawNoScaleNodes    = drawNoScaleNodes'
                            , _birchDrawLegendSep       = drawLegendSep'
                            , _birchDrawLegendAllLabels = DrawLegendAllLabels False
                            , _birchDrawPalette         = drawPalette'
                            , _birchDrawColors          = drawColors'
                            , _birchDrawDiscretize      = Nothing
                            , _birchDrawScaleSaturation = drawScaleSaturation'
                            , _birchDrawFont            = Nothing
                            , _birchTree                = tree
                            , _birchMat                 = mat
                            , _birchSimMat              = simMat
                            }
        in fmap (L.view L._1) $ mainDiagram config

-- | Build a `Diagram`-based user interface using an IO updatable.
graphicalUI'
    :: T.Text
    -> TS.Updatable (IO (D.Diagram D.Cairo))
    ->
       -- ^ Program logic
       IO ()
graphicalUI' = TS.ui setupGraphical processGraphicalEvent
  where
    setupGraphical :: Gtk.HBox -> IO Gtk.DrawingArea
    setupGraphical hBox = do
        drawingArea <- Gtk.drawingAreaNew
        Gtk.boxPackStart hBox drawingArea Gtk.PackGrow 0
        return drawingArea
    processGraphicalEvent :: Gtk.DrawingArea -> IO (D.Diagram D.Cairo) -> IO ()
    processGraphicalEvent drawingArea diagramIO = do
        diagram <- diagramIO

        drawWindow <- Gtk.widgetGetDrawWindow drawingArea
        (w, h) <- Gtk.widgetGetSize drawingArea

        let w' = fromIntegral (w :: Int) / 2
            h' = fromIntegral (h :: Int) / 2
            minSize = fromIntegral $ min w h
            maxIsWidth =
                if max (D.width diagram) (D.height diagram) == D.width diagram
                    then True
                    else False
            resizeFunc =
                bool (D.scaleUToY minSize) (D.scaleUToY minSize) maxIsWidth
            diagram' = diagram
                   D.# D.toGtkCoords
                   D.# resizeFunc
                   D.# D.center
                   D.# D.moveTo (D.p2 (w', h'))

        D.renderToGtk drawWindow diagram'

-- | Build a `Diagram`-based user interface using an IO updatable. Draws to a
-- png first.
graphicalUIPng'
    :: T.Text
    -> TS.Updatable (IO (D.Diagram D.Cairo))
    ->
       -- ^ Program logic
       IO ()
graphicalUIPng' title updatable = withTempFile "." "temp_tree.png" $ \file h -> do
    TS.ui setupGraphical (processGraphicalEvent file h) title updatable
  where
    setupGraphical :: Gtk.HBox -> IO Gtk.DrawingArea
    setupGraphical hBox = do
        drawingArea <- Gtk.drawingAreaNew
        Gtk.boxPackStart hBox drawingArea Gtk.PackGrow 0
        return drawingArea
    processGraphicalEvent :: FilePath -> IO.Handle -> Gtk.DrawingArea -> IO (D.Diagram D.Cairo) -> IO ()
    processGraphicalEvent file h drawingArea diagramIO = do
        dia <- diagramIO

        D.renderCairo
            file
            (D.mkHeight 1000)
            dia

        IO.hClose h

        diagram <- fmap (either mempty D.image) $ D.loadImageEmb file

        drawWindow <- Gtk.widgetGetDrawWindow drawingArea
        (w, h) <- Gtk.widgetGetSize drawingArea

        let w' = fromIntegral (w :: Int) / 2
            h' = fromIntegral (h :: Int) / 2
            minSize = fromIntegral $ min w h
            maxIsWidth =
                if max (D.width diagram) (D.height diagram) == D.width diagram
                    then True
                    else False
            resizeFunc =
                bool (D.scaleUToY minSize) (D.scaleUToY minSize) maxIsWidth
            diagram' = diagram
                   D.# D.toGtkCoords
                   D.# resizeFunc
                   D.# D.center
                   D.# D.moveTo (D.p2 (w', h'))

        D.renderToGtk drawWindow diagram'