telescope-0.3.0: src/Telescope/Fits/Encoding/MegaHeader.hs
{-# LANGUAGE OverloadedStrings #-}
-- {-# OPTIONS_HADDOCK hide #-}
module Telescope.Fits.Encoding.MegaHeader where
import Control.Monad (void)
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.ByteString.Char8 qualified as C8
import Data.Char (ord)
import Data.List qualified as L
import Data.List.NonEmpty qualified as NE
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.Encoding qualified as TE
import Data.Void (Void)
import Data.Word (Word8)
import Telescope.Data.Axes
import Telescope.Fits.BitPix
import Telescope.Fits.DataArray
import Telescope.Fits.HDU.Block (hduRecordLength)
import Telescope.Fits.Header hiding (FromKeyword (..), parseHeader, parseKeyword)
import Text.Megaparsec (ParseErrorBundle, Parsec, (<|>))
import Text.Megaparsec qualified as M
import Text.Megaparsec.Byte qualified as M
import Text.Megaparsec.Byte.Lexer qualified as MBL
import Text.Megaparsec.Error (ParseError (..), ParseErrorBundle (..))
import Text.Megaparsec.Error qualified as MP
import Text.Megaparsec.Pos qualified as MP
import Text.Megaparsec.State qualified as M
type Parser = Parsec Void ByteString
type ParseErr = ParseErrorBundle ByteString Void
-- | Runs a single parser and returns the remainder of the input
runNextParser :: String -> BS.ByteString -> Parser a -> Either ParseErr (a, BS.ByteString)
runNextParser src inp parse = do
let st1 = M.initialState src inp
case M.runParser' parse st1 of
(st2, Right a) -> do
-- only consumes input if it succeeds
let rest = BS.drop st2.stateOffset inp
pure (a, rest)
(_, Left err) -> Left err
showParseError :: ParseErr -> String
showParseError bundle =
let pos = bundle.bundlePosState
inp = pos.pstateInput
in L.intercalate "\n HI " (NE.toList (fmap (showError inp) bundle.bundleErrors))
where
showError :: ByteString -> ParseError ByteString Void -> String
showError inp err =
showCurrent inp (MP.errorOffset err) <> "\n " <> L.intercalate "\n " (lines (MP.parseErrorPretty err))
showCurrent inp off =
let line = floor @Float @Int (fromIntegral off / fromIntegral hduRecordLength)
col = off - (line * hduRecordLength)
in "HDU Header "
<> show line
<> " column "
<> show col
<> ": \n"
<> show (BS.take hduRecordLength $ BS.drop (line * hduRecordLength) inp)
toWord :: Char -> Word8
toWord = fromIntegral . ord
wordsText :: [Word8] -> Text
wordsText = TE.decodeUtf8 . BS.pack
-- | Consumes ALL header blocks until end, then all remaining space
parseHeader :: Parser Header
parseHeader = do
pairs <- M.manyTill parseRecordLine (M.string' "end")
M.space -- consume space padding all the way to the end of the next 2880 bytes header block
return $ Header pairs
parseRecordLine :: Parser HeaderRecord
parseRecordLine = do
M.try (Keyword <$> parseKeywordRecord)
<|> M.try (BlankLine <$ parseLineBlank)
<|> M.try (Comment <$> parseLineComment)
<|> M.try (Comment <$> parseLineCommentSpaces)
<|> (History <$> parseLineHistory)
parseLineHistory :: Parser Text
parseLineHistory = do
lineStart <- parsePos
_ <- M.string' "HISTORY "
M.space
untilLineEnd lineStart M.anySingle
parseKeywordRecord :: Parser KeywordRecord
parseKeywordRecord = do
((k, v), mc) <- withComments parseKeywordValue
pure $ KeywordRecord k v mc
-- | Parses the specified keyword
parseKeywordRecord' :: ByteString -> Parser a -> Parser a
parseKeywordRecord' k pval = ignoreComments $ do
_ <- M.string' k
parseEquals
pval
-- | Combinator to allow for parsing a record with inline comments
withComments :: Parser a -> Parser (a, Maybe Text)
withComments parse = do
-- assumes we are at the beginning of the line
lineStart <- parsePos
a <- parse
mc <- parseLineEnd lineStart
return (a, mc)
ignoreComments :: Parser a -> Parser a
ignoreComments parse = do
(a, _) <- withComments parse
pure a
parseKeywordValue :: Parser (Text, Value)
parseKeywordValue = do
key <- parseKeyword
parseEquals
val <- parseValue
return (key, val)
parseLineEnd :: Int -> Parser (Maybe Text)
parseLineEnd lineStart = do
M.try (Nothing <$ spacesToLineEnd lineStart) <|> (Just <$> parseInlineComment lineStart)
untilLineEnd :: Int -> Parser Word8 -> Parser Text
untilLineEnd lineStart parseChar = do
curr <- parsePos
let used = curr - lineStart
bs <- M.count (hduRecordLength - used) parseChar
return $ wordsText bs
spacesToLineEnd :: Int -> Parser ()
spacesToLineEnd lineStart = do
_ <- untilLineEnd lineStart (M.char $ toWord ' ')
pure ()
parseInlineComment :: Int -> Parser Text
parseInlineComment lineStart = do
M.space
_ <- M.char $ toWord '/'
_ <- M.optional charSpace
T.strip <$> untilLineEnd lineStart M.anySingle
where
charSpace = M.char $ toWord ' '
parseLineComment :: Parser Text
parseLineComment = do
let kw = "COMMENT " :: ByteString
_ <- M.string' kw
c <- M.count (hduRecordLength - BS.length kw) M.anySingle
return $ wordsText c
parseLineCommentSpaces :: Parser Text
parseLineCommentSpaces = do
-- Invalid comments used by JWST
lineStart <- parsePos
-- exactly 8 spaces
_ <- M.count 8 (M.satisfy (== toWord ' '))
T.strip <$> untilLineEnd lineStart M.anySingle
parseLineBlank :: Parser ()
parseLineBlank = do
_ <- withComments $ M.count' 0 80 (M.satisfy (== toWord ' '))
pure ()
-- | Anything but a space or equals
parseKeyword :: Parser Text
parseKeyword = wordsText <$> M.some (M.noneOf $ fmap toWord [' ', '='])
parseValue :: Parser Value
parseValue =
-- try is required here because Megaparsec doesn't automatically backtrack if the parser consumes anything
M.try (Float <$> parseFloat)
<|> M.try (Integer <$> parseInt)
<|> (Logic <$> parseLogic)
<|> (String <$> parseStringContinue)
parseInt :: (Num a) => Parser a
parseInt = MBL.signed M.space MBL.decimal
parseFloat :: Parser Double
parseFloat = MBL.signed M.space MBL.float
parseLogic :: Parser LogicalConstant
parseLogic = do
T <$ M.string' "T" <|> F <$ M.string' "F"
parseStringContinue :: Parser Text
parseStringContinue = do
t <- parseStringValue
mc <- M.optional $ M.try parseContinue
case mc of
Nothing -> return t
Just tc -> return $ T.dropWhileEnd (== '&') t <> tc
parseContinue :: Parser Text
parseContinue = do
M.space
lineStart <- parsePos
_ <- M.string' "CONTINUE"
M.space
more <- parseStringContinue
_ <- parseLineEnd lineStart
pure more
parseStringValue :: Parser Text
parseStringValue = do
-- The rules are weird, NULL means a NULL string, '' is an empty
-- string, a ' followed by a bunch of spaces and a close ' is
-- considered an empty string, and trailing whitespace is ignored
-- within the quotes, but not leading spaces.
ls <- M.between (M.char quote) (M.char quote) $ M.many $ M.anySingleBut quote
return (T.stripEnd $ wordsText ls)
where
-- consumeDead :: Parser ()
-- consumeDead = M.space >> skipEmpty
quote = toWord '\''
skipEmpty :: Parser ()
skipEmpty = void (M.many $ M.satisfy (toWord '\0' ==))
-- parseEnd :: Parser ()
-- parseEnd = M.string' "end" >> M.space <* M.eof
parseEquals :: Parser ()
parseEquals = M.space >> M.char (toWord '=') >> M.space
parsePos :: Parser Int
parsePos = MP.unPos . MP.sourceColumn <$> M.getSourcePos
parseBitPix :: Parser BitPix
parseBitPix = do
v <- parseKeywordRecord' "BITPIX" parseValue
toBitpix v
where
toBitpix (Integer 8) = return BPInt8
toBitpix (Integer 16) = return BPInt16
toBitpix (Integer 32) = return BPInt32
toBitpix (Integer 64) = return BPInt64
toBitpix (Integer (-32)) = return BPFloat
toBitpix (Integer (-64)) = return BPDouble
toBitpix _ = fail "Invalid BITPIX header"
parseNaxes :: Parser (Axes Column)
parseNaxes = do
n <- parseKeywordRecord' "NAXIS" parseInt
Axes <$> mapM parseN [1 .. n]
where
parseN :: Int -> Parser Int
parseN n = parseKeywordRecord' (C8.pack $ "NAXIS" <> show n) parseInt
-- | We don't parse simple here, because it isn't required on all HDUs
parseDimensions :: Parser Dimensions
parseDimensions = do
bp <- parseBitPix
Dimensions bp <$> parseNaxes
parsePrimaryKeywords :: Parser Dimensions
parsePrimaryKeywords = do
_ <- parseKeywordRecord' "SIMPLE" parseLogic
parseDimensions
parseImageKeywords :: Parser Dimensions
parseImageKeywords = do
_ <- ignoreComments $ M.string' "XTENSION= 'IMAGE '"
parseDimensions
parseBinTableKeywords :: Parser (Dimensions, Int)
parseBinTableKeywords = do
_ <- ignoreComments $ M.string' "XTENSION= 'BINTABLE'"
sz <- parseDimensions
pc <- parseKeywordRecord' "PCOUNT" parseInt
return (sz, pc)
-- parsePrimary :: Parser DataHDU
-- parsePrimary = do
-- -- do not consume the headers used for the dimensions
-- dm <- M.lookAhead parsePrimaryKeywords
-- hd <- parseHeader
-- dt <- parseMainData dm
-- return $ DataHDU hd (dataArray dm dt)
-- parseImage :: Parser DataHDU
-- parseImage = do
-- -- do not consume the headers used for the dimensions
-- dm <- M.lookAhead parseImageKeywords
-- hd <- parseHeader
-- dt <- parseMainData dm
-- return $ DataHDU hd (dataArray dm dt)
-- parseBinTable :: Parser BinTableHDU
-- parseBinTable = do
-- (dm, pc) <- M.lookAhead parseBinTableKeywords
-- hd <- parseHeader
-- dt <- parseMainData dm
-- hp <- parseBinTableHeap
-- return $ BinTableHDU hd pc hp (dataArray dm dt)
-- where
-- parseBinTableHeap = return ""
-- parseMainData :: Dimensions -> Parser ByteString
-- parseMainData size = do
-- let len = dataSizeBytes size
-- M.takeP (Just ("Data Array of " <> show len <> " Bytes")) (fromIntegral len)
--
--
-- parseExtensions :: Parser [Extension]
-- parseExtensions = do
-- M.many parseExtension
-- where
-- parseExtension :: Parser Extension
-- parseExtension =
-- Image <$> parseImage <|> BinTable <$> parseBinTable