packages feed

seqloc-datafiles-0.4: src/BedSubregion.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleContexts #-}
module Main
       where  

import Control.Applicative
import qualified Control.Exception.Lifted as E
import Control.Monad
import Control.Monad.Base
import Control.Monad.IO.Class
import qualified Control.Monad.Trans.Resource as R
import qualified Data.ByteString.Char8 as BS
import qualified Data.Conduit as C
import qualified Data.Conduit.Binary as CB
import qualified Data.Conduit.List as C
import Data.Maybe

import qualified Bio.SeqLoc.Bed as Bed
import Bio.SeqLoc.LocRepr
import qualified Bio.SeqLoc.Location as Loc
import Bio.SeqLoc.OnSeq
import qualified Bio.SeqLoc.Position as Pos
import qualified Bio.SeqLoc.SpliceLocation as Loc
import Bio.SeqLoc.Strand
import Bio.SeqLoc.Transcript

import System.Console.CmdTheLine
import System.FilePath
import System.IO
import qualified Text.PrettyPrint as PP

main :: IO ()
main = run ( bedsub, info )
  where info = defTI { termName = "bed-subregion"
                     , version = "2014-11-23"
                     , termDoc = "Process BED file to extract specific sub-regions"
                     }
        bedsub = bedSubregions <$> argConf

bedSubregions :: Conf -> IO ()
bedSubregions conf = R.runResourceT $
                     CB.sourceFile (cInput conf) C.$=
                     Bed.bedConduit C.$$
                     handleTranscripts conf

handleTranscripts :: (MonadIO m, R.MonadResource m) => Conf -> C.Sink Transcript m ()
handleTranscripts conf = C.bracketP (openFile (cOutFile conf) WriteMode) hClose loop
  where loop hout = C.head >>= maybe (return ()) (\t -> handleOne t >> loop hout)
          where handleOne t = maybe (return ()) writeSubregion $ regionSpliceLoc (cRegionSpec conf) t
                  where writeSubregion sl = let t' = t { location = (location t) { unOnSeq = sl }, cds = Nothing }
                                            in liftIO $ BS.hPutStrLn hout $ Bed.transcriptToBedStd t'
        
data TranscriptRegion = WholeTrx | Utr5 | Cds | Utr3 deriving (Show, Read, Eq, Ord, Bounded, Enum)

trxRegion :: TranscriptRegion -> Transcript -> Maybe Loc.ContigLoc
trxRegion WholeTrx trx = let sploc = unOnSeq . location $ trx
                         in Just $! Loc.fromPosLen (Pos.Pos 0 Plus) (Loc.length sploc)
trxRegion Utr5 trx = utr5 trx
trxRegion Utr3 trx = utr3 trx
trxRegion Cds trx = cds trx
                         
data TooLong = TooLongExtend | TooLongTruncate | TooLongDiscard deriving (Show, Read, Eq, Ord, Bounded, Enum)

handleEnds :: TooLong -> Loc.ContigLoc -> Loc.ContigLoc -> Maybe Loc.ContigLoc
handleEnds TooLongExtend _base cloc = Just cloc
handleEnds TooLongTruncate base cloc = let start = max (Pos.offset . Loc.startPos $ base) (Pos.offset . Loc.startPos $ cloc)
                                           end = min (Pos.offset . Loc.endPos $ base) (Pos.offset . Loc.endPos $ cloc)
                                       in if start < end
                                          then Just $! Loc.fromStartEnd start end
                                          else Nothing
handleEnds TooLongDiscard base cloc = if ((Pos.offset . Loc.startPos $ base) <= (Pos.offset . Loc.startPos $ cloc)
                                          && (Pos.offset . Loc.endPos $ base) >= (Pos.offset . Loc.endPos $ cloc))
                                      then Just cloc
                                      else Nothing

clocOutofExtended :: (Loc.Location l, LocRepr l) => Loc.ContigLoc -> l -> l
clocOutofExtended cloc l0 = fromMaybe badExtension $ Loc.clocOutof (Loc.slide startxtn cloc) lext
  where startxtn = negate . Pos.offset . Loc.startPos $ cloc
        endxtn = (Pos.offset . Loc.endPos $ cloc) + 1 - (Loc.length l0 - 1)
        lext = Loc.extend (startxtn, endxtn) l0
        badExtension = error . BS.unpack $ BS.unwords [ "Bad extension: ", repr cloc, " in ", repr l0, " to ", repr lext ]

data RegionSpec = RegionSpec { rsRegion :: !TranscriptRegion
                             , rsStartOffset :: !(Maybe Pos.Offset)
                             , rsLength :: !(Maybe Pos.Offset)
                             , rsEndOffset :: !(Maybe Pos.Offset)
                             , rsTooLong :: !TooLong
                             }
                deriving (Show)

validRegionSpec :: RegionSpec -> Bool
validRegionSpec (RegionSpec _rgn (Just _startoff) (Just _len) Nothing        _toolong) = True
validRegionSpec (RegionSpec _rgn (Just _startoff) Nothing     (Just _endoff) _toolong) = True
validRegionSpec (RegionSpec _rgn Nothing          (Just _len) (Just _endoff) _toolong) = True
validRegionSpec _ = False

regionSpliceLoc :: RegionSpec -> Transcript -> Maybe Loc.SpliceLoc
regionSpliceLoc (RegionSpec rgn (Just startoff) (Just len) Nothing toolong) trx
  = do base <- trxRegion rgn trx
       cloc <- handleEnds toolong base $ Loc.fromPosLen (Pos.slide (Loc.startPos base) startoff) len
       return $! clocOutofExtended cloc (unOnSeq . location $ trx)
regionSpliceLoc (RegionSpec rgn (Just startoff) Nothing (Just endoff) toolong) trx
  = do base <- trxRegion rgn trx
       let start = (Pos.offset . Loc.startPos $ base) + startoff
           end = (Pos.offset . Loc.endPos $ base) + endoff
       if start <= end
          then do cloc <- handleEnds toolong base $ Loc.fromStartEnd start end
                  return $! clocOutofExtended cloc (unOnSeq . location $ trx)
          else Nothing
regionSpliceLoc (RegionSpec rgn Nothing (Just len) (Just endoff) toolong) trx
  = do base <- trxRegion rgn trx
       let end = (Pos.offset . Loc.endPos $ base) + endoff
           start = 1 + end - len
       if start <= end
          then do cloc <- handleEnds toolong base $ Loc.fromStartEnd start end
                  return $! clocOutofExtended cloc (unOnSeq . location $ trx)
          else Nothing
regionSpliceLoc rs _ = error $ "Invalid region selection " ++ show rs

data Conf = Conf { cInput :: !FilePath
                 , cOutput :: !(Maybe FilePath)
                 , cRegionSpec :: !RegionSpec
                 }

cOutFile :: Conf -> FilePath
cOutFile conf = fromMaybe defaultOutput . cOutput $ conf
  where defaultOutput = (dropExtension . cInput $ conf) ++ "_subregion" ++ (takeExtension . cInput $ conf)

argConf :: Term Conf
argConf = Conf <$> argInput <*> argOutput <*> regionspec

argInput :: Term FilePath
argInput = required $ opt Nothing $ ( optInfo [ "i" ])
  { optName = "INPUT.BED", optDoc = "BED input" }

argOutput :: Term (Maybe FilePath)
argOutput = value $ opt Nothing $ ( optInfo [ "o" ])
  { optName = "OUTPUT.BED", optDoc = "BED output" }

argRegion :: Term TranscriptRegion
argRegion = ret . fmap validate . value $
            vFlag Nothing $
            [ (Just WholeTrx, (optInfo [ "whole-trx" ]) { optDoc = "Region relative to whole transcript" })
            , (Just Utr5,     (optInfo [ "utr5" ])      { optDoc = "Region relative to 5' UTR" })
            , (Just Utr3,     (optInfo [ "utr3" ])      { optDoc = "Region relative to 3' UTR" })
            , (Just Cds,      (optInfo [ "cds" ])       { optDoc = "Region relative to CDS" })
            ]
  where validate :: Maybe TranscriptRegion -> Err TranscriptRegion
        validate = maybe noRegion return
        noRegion = msgFail . PP.text $
                   "Specify a reference transcription region (whole transcript, CDS, etc.)"

argTooLong :: Term TooLong
argTooLong = ret . fmap validate . value $
             vFlag Nothing $
             [ (Just TooLongExtend,   (optInfo [ "extend" ])   { optDoc = "Extend beyond reference region" })
             , (Just TooLongTruncate, (optInfo [ "truncate" ]) { optDoc = "Truncate to lie within reference region " })
             , (Just TooLongDiscard,  (optInfo [ "discard" ])  { optDoc = "Discard when lying outside reference region" })
             ]
  where validate :: Maybe TooLong -> Err TooLong
        validate = maybe noarg return
        noarg = msgFail . PP.text $
                "Specify how subregions extending outside the reference region should be handled (truncation etc.)"

argStartOffset :: Term (Maybe Int)
argStartOffset = value $ opt Nothing $ ( optInfo [ "start-off" ]) { optName = "DELTA-START", optDoc = "Offset of start position, positive is more 3'" }

argEndOffset :: Term (Maybe Int)
argEndOffset = value $ opt Nothing $ ( optInfo [ "end-off" ]) { optName = "DELTA-END", optDoc = "Offset of end position, positive is more 3'" }

argLength :: Term (Maybe Int)
argLength = value $ opt Nothing $ ( optInfo [ "length" ]) { optName = "LENGTH", optDoc = "Length of the sub-region" }

regionspec :: Term RegionSpec
regionspec = ret . fmap validate $
             RegionSpec <$>
             argRegion <*>
             (liftM fromIntegral <$> argStartOffset) <*>
             (liftM fromIntegral <$> argLength) <*>
             (liftM fromIntegral <$> argEndOffset) <*>
             argTooLong
  where validate :: RegionSpec -> Err RegionSpec
        validate rs = if validRegionSpec rs
                      then return rs
                      else msgFail . PP.text $
                           "Specify exactly two of start offset, length, and end offset"

throwerr :: (MonadBase IO m) => String -> m a
throwerr = E.ioError . userError