packages feed

gruff-0.1: gruff.hs

{-# LANGUAGE Rank2Types, ScopedTypeVariables #-}
module Main (main) where

import Prelude hiding (catch, log)
import Control.Monad (forM_, liftM2)
import Data.IORef (IORef, newIORef, readIORef, atomicModifyIORef, writeIORef)
import Data.Maybe (isJust, fromMaybe)
import Data.Ratio (numerator, denominator)
import Graphics.UI.Gtk hiding (get, Region, Size)
import Graphics.UI.Gtk.OpenGL (initGL)
import System.Directory
  ( getAppUserDataDirectory, createDirectoryIfMissing )
import System.FilePath ((</>))
import Text.PrettyPrint.Leijen.Text (pretty)
import Text.FShow.Raw (DecimalFormat, nanTest, infTest)

import Numeric.QD
import Fractal.RUFF.Mandelbrot.Address (AngledInternalAddress, Angle, parse, angledInternalAddress, externalAngles)
import Fractal.RUFF.Mandelbrot.Ray (externalRay)
import Fractal.RUFF.Types.Complex (Complex((:+)), magnitude)

import GLUTGtk (glut, Size(Size))
import PeriodScan (periodScan, periodNucleus)
import Nucleus (refineNucleus)
import Address (parseAngle, parseAngles, splitAddress, joinAddress, addressPeriod)
import Logger (logger, LogLevel(Debug))
import qualified Logger as Log
import Utils (safeRead, catchIO)
import Paths_gruff (getDataFileName)
import Browser
import StatusDialog
import CacheView (cInitialize)

exit :: (LogLevel -> String -> IO ()) -> FilePath -> Gruff -> IO ()
exit lg stateFile g = do
  lg Debug "exitCallback"
  aDoSave stateFile g
  mainQuit

data GruffGUI = GruffGUI
  { dStatus                 :: StatusDialog
  -- buttons
  , bHome
  , bLoad
  , bSave
  , bAddressToCoordinates
  , bAddressToIslandChild
  , bIslandChildToAddress
  , bAddressToAngles
  , bIslandToAngles
  , bLowerAngleToAddress
  , bUpperAngleToAddress
  , bPeriodScan             :: Button
  -- entries
  , eAddress
  , eIsland
  , eChild
  , eLowerAngle
  , eUpperAngle
  , eReal
  , eImag
  , eSize
  , eHueShift
  , eHueScale               :: Entry
  -- windows
  , wMain
  , wImage                  :: Window
  }

main :: IO ()
main = do
  -- contexts
  _ <- initGUI
  _ <- initGL
  gl' <- glut minSize
  glC <- glut minSize
  -- directories
  appDir <- getAppUserDataDirectory "gruff"
  let cacheDir' = appDir </> "cache"
      logDir    = appDir </> "log"
      stateFile = appDir </> "state.gruff"
  createDirectoryIfMissing False appDir
  createDirectoryIfMissing False logDir
  lg <- logger logDir
  icon <- pixbufNewFromFile =<< getDataFileName "icon.png"
  -- widget window
  (iw, iUpdate, iInitializeLate) <- iInitialize gl' icon lg cacheDir'
  (cw, cUpdate, cInitializeLate) <- cInitialize glC icon cacheDir'
  -- widget window
  sg <- sizeGroupNew SizeGroupHorizontal
  let spacing = 2
      entryNewWithMnemonic m = do
        e <- entryNew
        entrySetWidthChars e 80
        l <- labelNewWithMnemonic m
        labelSetMnemonicWidget l e
        sizeGroupAddWidget sg l
        h <- hBoxNew False spacing
        boxPackStart h l PackNatural 0
        boxPackStartDefaults h e
        return (e, h)
      frameNewWithContents t r ws = do
        f <- frameNew
        frameSetLabel f t
        frameSetLabelAlign f (if r then 1 else 0) 0.5
        v <- vBoxNew False spacing
        forM_ ws $ boxPackStartDefaults v
        set f [ containerChild := v ]
        return f
  dStatus'                  <- statusDialogNew
  b01@bHome'                <- buttonNewWithLabel "Home"
  b02@bLoad'                <- buttonNewWithLabel "Load"
  b03@bSave'                <- buttonNewWithLabel "Save"
  b7@bAddressToCoordinates' <- buttonNewWithLabel "Address → Coordinates"
  b1@bAddressToIslandChild' <- buttonNewWithLabel "Address → Island + Child"
  b2@bIslandChildToAddress' <- buttonNewWithLabel "Island + Child → Address"
  b3@bAddressToAngles'      <- buttonNewWithLabel "Address → Lower + Upper"
  b4@bIslandToAngles'       <- buttonNewWithLabel "Island → Lower + Upper"
  b5@bLowerAngleToAddress'  <- buttonNewWithLabel "Lower → Address"
  b6@bUpperAngleToAddress'  <- buttonNewWithLabel "Upper → Address"
  b8@bPeriodScan'           <- buttonNewWithLabel "Period Scan"
  (eAddress', fa1) <- entryNewWithMnemonic "_Address"
  (eIsland',  fa2) <- entryNewWithMnemonic "I_sland"
  (eChild',   fa3) <- entryNewWithMnemonic "_Child"
  (eLowerAngle', fe1) <- entryNewWithMnemonic "_Lower"
  (eUpperAngle', fe2) <- entryNewWithMnemonic "_Upper"
  (eReal', fc1) <- entryNewWithMnemonic "_Real"
  (eImag', fc2) <- entryNewWithMnemonic "_Imag"
  (eSize', fc3) <- entryNewWithMnemonic "Si_ze"
  (eHueShift', fh1) <- entryNewWithMnemonic "Hue Shift"
  (eHueScale', fh2) <- entryNewWithMnemonic "Hue Scale"
  b0 <- hBoxNew False spacing
  mapM_ (boxPackStartDefaults b0) [b01, b02, b03]
  fb <- frameNewWithContents "Actions" False $ toWidget b0 : map toWidget [b7, b1, b2, b3, b4, b5, b6, b8]
  fa <- frameNewWithContents "Angled Internal Address" True [fa1, fa2, fa3]
  fe <- frameNewWithContents "External Angles" True [fe1, fe2]
  fh <- frameNewWithContents "Colouring" True [fh1, fh2]
  fc <- frameNewWithContents "Coordinates" True [fc1, fc2, fc3]
  v <- vBoxNew False spacing
  mapM_ (boxPackStartDefaults v) [fc, fa, fe, fh]
  h <- hBoxNew False spacing
  boxPackStartDefaults h fb
  boxPackStartDefaults h v
  ww <- windowNew
  set ww [ windowIcon := Just icon, windowTitle := "gruff control" ]
  containerAdd ww h
  mg <- aDoLoad stateFile
  gR <- newIORef $ case mg of
    Nothing -> initialGruff
    Just g -> g
  let g0 = GruffGUI
            { dStatus               = dStatus'
            , bHome                 = bHome'
            , bLoad                 = bLoad'
            , bSave                 = bSave'
            , bAddressToCoordinates = bAddressToCoordinates'
            , bAddressToIslandChild = bAddressToIslandChild'
            , bIslandChildToAddress = bIslandChildToAddress'
            , bAddressToAngles      = bAddressToAngles'
            , bIslandToAngles       = bIslandToAngles'
            , bLowerAngleToAddress  = bLowerAngleToAddress'
            , bUpperAngleToAddress  = bUpperAngleToAddress'
            , bPeriodScan           = bPeriodScan'
            , eAddress              = eAddress'
            , eIsland               = eIsland'
            , eChild                = eChild'
            , eLowerAngle           = eLowerAngle'
            , eUpperAngle           = eUpperAngle'
            , eReal                 = eReal'
            , eImag                 = eImag'
            , eSize                 = eSize'
            , eHueShift             = eHueShift'
            , eHueScale             = eHueScale'
            , wMain                 = ww
            , wImage                = iw
            }
      but b a = do
        _ <- b g0 `onClicked` wrapA g0 gR a
        return ()
      butI b a = do
        _ <- b g0 `onClicked` (wrapA g0 gR a >> upI)
        return ()
      butJ b a = do
        _ <- b g0 `onClicked` (wrapA' g0 gR a upI)
        return ()
      ent e a = do
        _ <- e g0 `onEntryActivate` (entryGetText (e g0) >>= wrapE g0 gR a)
        return ()
      entI e a = do
        _ <- e g0 `onEntryActivate` (entryGetText (e g0) >>= wrapE g0 gR a >> upI)
        return ()
      upI = do
        g <- readIORef gR
        iUpdate (gReal g) (gImag g) (gSize g) (gHueShift g) (gHueScale g)
        cUpdate (gReal g) (gImag g) (gSize g)
      aUpdate re im z = do
        atomicModifyIORef gR $ \g ->
          ( g{ gReal = Just re, gImag = Just im, gSize = Just z }, () )
        g <- readIORef gR
        uReal g0 g
        uImag g0 g
        uSize g0 g
        cUpdate (gReal g) (gImag g) (gSize g)
  butI bHome aHome
  butI bLoad aLoad
  but  bSave aSave
  butJ bAddressToCoordinates aAddressToCoordinates
  but  bAddressToIslandChild aAddressToIslandChild
  but  bIslandChildToAddress aIslandChildToAddress
  but  bAddressToAngles      aAddressToAngles
  but  bIslandToAngles       aIslandToAngles
  but  bLowerAngleToAddress  aLowerAngleToAddress
  but  bUpperAngleToAddress  aUpperAngleToAddress
  butJ bPeriodScan           aPeriodScan
  ent  eAddress              aAddress
  ent  eIsland               aIsland
  ent  eChild                aChild
  ent  eLowerAngle           aLowerAngle
  ent  eUpperAngle           aUpperAngle
  entI eReal aReal
  entI eImag aImag
  entI eSize aSize
  entI eHueShift aHueShift
  entI eHueScale aHueScale
  let aExit' = (exit (Log.log lg) stateFile =<< readIORef gR)
  iInitializeLate aUpdate aExit'
  cInitializeLate         aExit'
  _ <- ww `onDestroy` aExit'
  g <- readIORef gR
  uEverything g0 g
  upI
  refreshGUI g0 g
  widgetShowAll ww
  widgetShowAll cw
  widgetShowAll iw
  mainGUI

data Gruff = Gruff
  { gAddress    :: Maybe AngledInternalAddress
  , gIsland     :: Maybe AngledInternalAddress
  , gChild      :: Maybe [Angle]
  , gLowerAngle :: Maybe Angle
  , gUpperAngle :: Maybe Angle
  , gReal       :: Maybe QuadDouble
  , gImag       :: Maybe QuadDouble
  , gSize       :: Maybe QuadDouble
  } | Gruff1
  { gAddress    :: Maybe AngledInternalAddress
  , gIsland     :: Maybe AngledInternalAddress
  , gChild      :: Maybe [Angle]
  , gLowerAngle :: Maybe Angle
  , gUpperAngle :: Maybe Angle
  , gReal       :: Maybe QuadDouble
  , gImag       :: Maybe QuadDouble
  , gSize       :: Maybe QuadDouble
  , gHueShift   :: Maybe Double
  , gHueScale   :: Maybe Double
  }
  deriving (Read, Show)

initialGruff :: Gruff
initialGruff = Gruff1
  { gAddress    = parse "1"
  , gIsland     = parse "1"
  , gChild      = Just []
  , gLowerAngle = Just 0
  , gUpperAngle = Just 1
  , gReal       = Just 0
  , gImag       = Just 0
  , gSize       = Just 1
  , gHueShift   = Just 0
  , gHueScale   = Just 1
  }

refreshGUI :: GruffGUI -> Gruff -> IO ()
refreshGUI g0 g = do
  can bAddressToCoordinates $ j gAddress
  can bAddressToIslandChild $ j gAddress
  can bIslandChildToAddress $ j gIsland && j gChild
  can bAddressToAngles      $ j gAddress
  can bIslandToAngles       $ j gIsland
  can bLowerAngleToAddress  $ j gLowerAngle
  can bUpperAngleToAddress  $ j gUpperAngle
  can bPeriodScan           $ True
  where
    can w = widgetSetSensitive (w g0)
    j a = isJust (a g)

-- button actions

type A = GruffGUI -> Gruff -> IO Gruff
type A' = GruffGUI -> Gruff -> (Gruff -> IO ()) -> IO ()

wrapA :: GruffGUI -> IORef Gruff -> A -> IO ()
wrapA g0 gR a = do
  g <- readIORef gR
  g' <- a g0 g
  writeIORef gR $! g'
  refreshGUI g0 g'

wrapA' :: GruffGUI -> IORef Gruff -> A' -> IO () -> IO ()
wrapA' g0 gR a upI = do
  g <- readIORef gR
  a g0 g $ \g' -> postGUISync $ do
    writeIORef gR $! g'
    upI
    refreshGUI g0 g'

aHome :: A
aHome g0 _ = do
  let g = initialGruff
  uEverything g0 g
  return g

aDoLoad :: FilePath -> IO (Maybe Gruff)
aDoLoad ff = (do
    gr <- safeRead `fmap` readFile ff
    return $ case gr of
      Just (Gruff a b c d e f g h) -> Just (Gruff1 a b c d e f g h (Just 0) (Just 1))
      g -> g
  ) `catchIO` const (return Nothing)

aLoad :: A
aLoad g0 g = do
  fc <- fileChooserDialogNew (Just "gruff load") (Just $ wMain g0) FileChooserActionOpen [("Load", ResponseAccept), ("Cancel", ResponseCancel)]
  widgetShow fc
  r <- dialogRun fc
  g' <- case r of
    ResponseAccept -> do
      mf <- fileChooserGetFilename fc
      case mf of
        Nothing -> return g
        Just f -> do
          mg <- aDoLoad f
          case mg of
            Nothing -> return g
            Just g' -> uEverything g0 g' >> return g'
    _ -> return g
  widgetDestroy fc
  return g'

aDoSave :: FilePath -> Gruff -> IO ()
aDoSave f g = writeFile f (show g) `catchIO` const (return ())

aSave :: A
aSave g0 g = do
  fc <- fileChooserDialogNew (Just "gruff save") (Just $ wMain g0) FileChooserActionSave [("Save", ResponseAccept), ("Cancel", ResponseCancel)]
  widgetShow fc
  r <- dialogRun fc
  case r of
    ResponseAccept -> do
      mf <- fileChooserGetFilename fc
      case mf of
        Nothing -> return ()
        Just f -> aDoSave f g
    _ -> return ()
  widgetDestroy fc
  return g

aPeriodScan :: A'
aPeriodScan g0 g gn = do
  statusDialog (dStatus g0) "gruff status" $ \progress -> do
    progress "Scanning for period..."
    let ps = do
          re <- gReal g
          im <- gImag g
          r  <- gSize g
          let c = re :+ im
          p <- periodScan r c
          return (p, r, c)
    g' <- case ps of
      Just (p, r, c) -> do
        progress $ "Scanning for nucleus (" ++ show p ++ ")..."
        case periodNucleus p r c of
          Just n -> do
            progress $ "Computing nucleus (" ++ show p ++ ")..."
            case refineNucleus p n of
              (z, (re :+ im)) | 10 > z && z > 0 ->
                return g{ gReal = Just re, gImag = Just im, gSize = Just (z * 4) }
              _ -> return g{ gReal = Nothing, gImag = Nothing, gSize = Nothing }
          _ -> return g{ gReal = Nothing, gImag = Nothing, gSize = Nothing }
      _ -> return g{ gReal = Nothing, gImag = Nothing, gSize = Nothing }
    progress "Done!"
    postGUISync $ do
      uReal g0 g'
      uImag g0 g'
      uSize g0 g'
    gn g'

aAddressToCoordinates :: A'
aAddressToCoordinates g0 g gn = do
  statusDialog (dStatus g0) "gruff status" $ \progress -> do
    progress "Splitting address..."
    g1 <- aAddressToIslandChild g0 g
    progress "Finding external angles..."
    g2 <- aIslandToAngles g0 g1
    progress "Tracing external rays..."
    g' <- aAnglesChildToCoordinates (progress "Computing nucleus...") g0 g2
    progress "Done!"
    postGUISync $ do
      uReal g0 g'
      uImag g0 g'
      uSize g0 g'
    gn g'

aAnglesChildToCoordinates :: IO () -> A
aAnglesChildToCoordinates progress _g0 g = do
  case liftM2 (,) (gIsland g) (gLowerAngle g) of
    Just (a, lo) ->
      let eps = 2 ** negate 48
          eps' = 2 ** negate 40
          p = addressPeriod a
          rlo = externalRay eps 8 (2**24) lo
          ok' w = not (nanTest w || infTest w)
          ok (w:+x) = ok' w && ok' x
          converge [] = Nothing
          converge [x] = Just x
          converge (x:m@(y:_))
            | not $ magnitude (x - y) < eps' = converge m
            | otherwise = Just x
          rend = converge . takeWhile ok $ rlo
      in  case rend of
        Just c -> progress >> case refineNucleus p c of
          (z, (re :+ im)) | z > 0 ->
            return g{ gReal = Just re, gImag = Just im, gSize = Just (z * 4) }
          _ -> return g{ gReal = Nothing, gImag = Nothing, gSize = Nothing }
        Nothing -> return g{ gReal = Nothing, gImag = Nothing, gSize = Nothing }
    Nothing -> return g{ gReal = Nothing, gImag = Nothing, gSize = Nothing }

aAddressToIslandChild :: A
aAddressToIslandChild g0 g = do
  let g' = fromMaybe g{ gIsland = Nothing, gChild = Nothing } $ do
        a <- gAddress g
        (i, c) <- splitAddress a
        return g{ gIsland = Just i, gChild = Just c }
  postGUISync $ uIsland g0 g'
  postGUISync $ uChild g0 g'
  return g'

aIslandChildToAddress :: A
aIslandChildToAddress g0 g = do
  let g' = g{ gAddress = liftM2 joinAddress (gIsland g) (gChild g) }
  postGUISync $ uAddress g0 g'
  return g'

aAddressToAngles :: A
aAddressToAngles g0 g = do
  let (l, u) = case externalAngles =<< gAddress g of
                  Just (lo, up) -> (Just lo, Just up)
                  Nothing -> (Nothing, Nothing)
      g' = g{ gLowerAngle = l, gUpperAngle = u }
  postGUISync $ uLowerAngle g0 g'
  postGUISync $ uUpperAngle g0 g'
  return g'

aIslandToAngles :: A
aIslandToAngles g0 g = do
  let (l, u) = case externalAngles =<< gIsland g of
                  Just (lo, up) -> (Just lo, Just up)
                  Nothing -> (Nothing, Nothing)
      g' = g{ gLowerAngle = l, gUpperAngle = u }
  -- strictness hack to prevent evaluation in gui thread...
  case (gLowerAngle g', gUpperAngle g') of
    (Just lo, Just up) | lo + up > 0 -> return ()
    _ -> return ()
  postGUISync $ uLowerAngle g0 g'
  postGUISync $ uUpperAngle g0 g'
  return g'

aLowerAngleToAddress :: A
aLowerAngleToAddress g0 g = do
  let a = angledInternalAddress =<< gLowerAngle g
      g' = g{ gAddress = a }
  postGUISync $ uAddress g0 g'
  return g'

aUpperAngleToAddress :: A
aUpperAngleToAddress g0 g = do
  let a = angledInternalAddress =<< gUpperAngle g
      g' = g{ gAddress = a }
  postGUISync $ uAddress g0 g'
  return g'

-- entry update

type U = GruffGUI -> Gruff -> IO ()

uEverything, uAddress, uIsland, uChild, uLowerAngle, uUpperAngle, uReal, uImag, uSize :: U
uEverything g0 g = forM_ [uAddress, uIsland, uChild, uLowerAngle, uUpperAngle, uReal, uImag, uSize] $ \u -> u g0 g
uAddress g0 g = do
  let s = filter ('"'/=) . show . pretty
  entrySetText (eAddress g0) (maybe "" s (gAddress g))
uIsland g0 g = do
  let s = filter ('"'/=) . show . pretty
  entrySetText (eIsland g0) (maybe "" s (gIsland g))
uChild g0 g = do
  let s x = show (numerator x) ++ "/" ++ show (denominator x)
      t = unwords . map s
  entrySetText (eChild g0) (maybe "" t (gChild g))
uLowerAngle g0 g = do
  let s x = show (numerator x) ++ " / " ++ show (denominator x)
  entrySetText (eLowerAngle g0) (maybe "" s (gLowerAngle g))
uUpperAngle g0 g = do
  let s x = show (numerator x) ++ " / " ++ show (denominator x)
  entrySetText (eUpperAngle g0) (maybe "" s (gUpperAngle g))
uReal g0 g = entrySetText (eReal g0) (maybe "" show (gReal g))
uImag g0 g = entrySetText (eImag g0) (maybe "" show (gImag g))
uSize g0 g = entrySetText (eSize g0) (maybe "" show (gSize g))

-- entry actions

type E = Gruff -> String -> Gruff

wrapE :: GruffGUI -> IORef Gruff -> E -> String -> IO ()
wrapE g0 gR e s = do
  g <- readIORef gR
  let g' = e g s
  writeIORef gR $! g'
  refreshGUI g0 g'

aAddress, aIsland, aChild, aLowerAngle, aUpperAngle, aReal, aImag, aSize, aHueShift, aHueScale :: E
aAddress    g s = g{ gAddress    = parse       s }
aIsland     g s = g{ gIsland     = parse       s }
aChild      g s = g{ gChild      = parseAngles s }
aLowerAngle g s = g{ gLowerAngle = parseAngle  s }
aUpperAngle g s = g{ gUpperAngle = parseAngle  s }
aReal       g s = g{ gReal       = safeRead    s }
aImag       g s = g{ gImag       = safeRead    s }
aSize       g s = g{ gSize       = safeRead    s }
aHueShift   g s = g{ gHueShift   = safeRead    s }
aHueScale   g s = g{ gHueScale   = safeRead    s }

minSize :: Size
minSize = Size 160 100