packages feed

patch-image-0.3.3.2: src/State.hs

module State where

import Degree (Degree(Degree))

import qualified Data.Csv as Csv
import qualified Data.HashMap.Strict as HashMap
import qualified Data.ByteString.Lazy.Char8 as BL
import qualified Data.ByteString.Char8 as B
import qualified Data.Traversable as Trav
import qualified Data.Foldable as Fold
import qualified Data.Vector as Vector
import qualified Data.List.HT as ListHT
import qualified Data.List as List
import qualified Data.Map as Map
import qualified Data.Set as Set
import Data.Traversable (traverse)
import Data.Foldable (foldMap)
import Data.Csv ((.=), (.:))
import Data.Vector (Vector)
import Data.Monoid ((<>))
import Data.Maybe (fromMaybe, catMaybes, isNothing)
import Data.Tuple.HT (mapFst, mapSnd, mapPair, swap)
import Data.Map (Map)

import qualified Control.Monad.Exception.Synchronous as ME
import qualified Control.Functor.HT as FuncHT
import Control.Monad (when, join)
import Control.Applicative (pure, liftA2, (<$>), (<$), (<*>), empty)

import Text.Printf (printf)

import qualified System.IO as IO
import Prelude hiding (read)


newtype File = File FilePath

data Angle = Angle FilePath (Degree Float)

data Position = Position FilePath (Degree Float) (Float, Float)

data Displacement =
      Displacement FilePath FilePath (Maybe Relation) (Maybe (Float, Float))

data Rotated =
      Rotated
         (Maybe ((FilePath, FilePath), Maybe Relation))
         (Maybe ((Float, Float), (Float, Float)))


imageId, angleId, dAngleId, xId, yId :: B.ByteString
imageId = B.pack "Image"
angleId = B.pack "Angle"
dAngleId = B.pack "DAngle"
xId = B.pack "X"
yId = B.pack "Y"

imageAId, imageBId, relationId, dxId, dyId :: B.ByteString
imageAId = B.pack "ImageA"
imageBId = B.pack "ImageB"
relationId = B.pack "Rel"
dxId = B.pack "DX"
dyId = B.pack "DY"

xaId, yaId, xbId, ybId :: B.ByteString
xaId = B.pack "XA"
yaId = B.pack "YA"
xbId = B.pack "XB"
ybId = B.pack "YB"


instance Csv.ToNamedRecord File where
   toNamedRecord (File path) = Csv.namedRecord [imageId .= path]
instance Csv.DefaultOrdered File where
   headerOrder _ = Csv.header [imageId]

instance Csv.ToNamedRecord Angle where
   toNamedRecord (Angle path (Degree angle)) =
      Csv.namedRecord [imageId .= path, angleId .= angle]
instance Csv.DefaultOrdered Angle where
   headerOrder _ = Csv.header [imageId, angleId]

data Relation = NonOverlapping | Overlapping
   deriving (Eq, Ord, Enum)

instance Csv.ToField Relation where
   toField NonOverlapping = B.singleton '-'
   toField Overlapping = B.singleton 'X'

instance Csv.FromField Relation where
   parseField bstr =
      case B.unpack bstr of
         "-" -> pure NonOverlapping
         "X" -> pure Overlapping
         _ -> empty

unrelated :: Bool -> Relation
unrelated b =
   if b
     then State.NonOverlapping
     else State.Overlapping


instance Csv.ToNamedRecord Position where
   toNamedRecord (Position path (Degree angle) (x,y)) =
      Csv.namedRecord [imageId .= path, angleId .= angle, xId .= x, yId .= y]
instance Csv.DefaultOrdered Position where
   headerOrder _ = Csv.header [imageId, angleId, xId, yId]

instance Csv.ToNamedRecord Displacement where
   toNamedRecord (Displacement pathA pathB rel disp) =
      Csv.namedRecord
         [imageAId .= pathA, imageBId .= pathB,
          relationId .= rel, dxId .= fmap fst disp, dyId .= fmap snd disp]
instance Csv.DefaultOrdered Displacement where
   headerOrder _ = Csv.header [imageAId, imageBId, relationId, dxId, dyId]

instance Csv.FromNamedRecord Displacement where
   parseNamedRecord m =
      Displacement
         <$> m .: imageAId
         <*> m .: imageBId
         <*> m .: relationId
         <*> runCombinedParser (parseMaybePair (m .:# dxId) (m .:# dyId))


instance Csv.ToNamedRecord Rotated where
   toNamedRecord (Rotated pathsRel rot) =
      let ((pathA, pathB), rel) = mapFst FuncHT.unzip $ FuncHT.unzip pathsRel
          ((xa,ya),(xb,yb)) =
            mapPair (FuncHT.unzip, FuncHT.unzip) $ FuncHT.unzip rot
      in  Csv.namedRecord $
            [imageAId .= pathA, imageBId .= pathB, relationId .= join rel,
             xaId .= xa, yaId .= ya, xbId .= xb, ybId .= yb]
instance Csv.DefaultOrdered Rotated where
   headerOrder _ =
      Csv.header [imageAId, imageBId, relationId, xaId, yaId, xbId, ybId]


{- |
We use it like a @Compose Writer Csv.Parser@,
we do not have much use for the 'Applicative' combinators.
-}
data NamedParser a = NamedParser [B.ByteString] (Csv.Parser a)

(.:#) :: Csv.FromField a => Csv.NamedRecord -> B.ByteString -> NamedParser a
(.:#) m name = NamedParser [name] (m .: name)

enumerateNames :: [B.ByteString] -> String
enumerateNames = B.unpack . B.intercalate (B.pack ", ")

parseMaybePair ::
   NamedParser (Maybe a) -> NamedParser (Maybe b) ->
   NamedParser (Maybe (a, b))
parseMaybePair (NamedParser na pa) (NamedParser nb pb) =
   let n = na++nb
   in  NamedParser n $ do
         m <- liftA2 (,) pa pb
         case m of
            (Nothing, Nothing) -> return Nothing
            (Just a, Just b) -> return $ Just (a,b)
            _ ->
               fail $
               printf "The columns %s must be all set or all empty." $
               enumerateNames n

parseImpliedMaybe ::
   NamedParser (Maybe a) -> NamedParser (Maybe b) ->
   NamedParser (Maybe (a, Maybe b))
parseImpliedMaybe (NamedParser na pa) (NamedParser nb pb) =
   NamedParser (na++nb) $ do
      m <- liftA2 (,) pa pb
      case m of
         (Nothing, Just _) ->
            fail $
            printf "If the columns %s are set, then %s must be set as well."
               (enumerateNames nb) (enumerateNames na)
         (ma,mb) -> return $ flip (,) mb <$> ma

runCombinedParser :: NamedParser a -> Csv.Parser a
runCombinedParser (NamedParser _ p) = p


instance Csv.FromNamedRecord Rotated where
   parseNamedRecord m =
      Rotated
         <$>
            (runCombinedParser $
             parseImpliedMaybe
               (parseMaybePair (m .:# imageAId) (m .:# imageBId))
               (m .:# relationId))
         <*>
            (runCombinedParser $
             parseMaybePair
               (parseMaybePair (m .:# xaId) (m .:# yaId))
               (parseMaybePair (m .:# xbId) (m .:# ybId)))

segmentRotated ::
   [Rotated] ->
   ME.Exceptional String
      [((FilePath, FilePath),
        (Maybe Relation, [((Float, Float), (Float, Float))]))]
segmentRotated =
   (\(prefix, blocks) ->
      ME.assert "leading relations without image header" (null prefix)
      >>
      (ME.Success $
       map
         (\((mrot0,(paths,mrel)), rots) ->
            (paths,
             (mrel,
              catMaybes $ mrot0 : map (\(Rotated _ mrot) -> mrot) rots)))
         blocks))
   .
   ListHT.segmentBeforeJust
      (\(Rotated mPathRel mrot) -> (,) mrot <$> mPathRel)

imagePairMap ::
   [((FilePath, FilePath), a)] ->
   ME.Exceptional String (Map (FilePath, FilePath) a)
imagePairMap =
   Trav.sequence .
   Map.fromListWithKey
      (\(pathA,pathB) _ _ ->
         ME.Exception $ printf "duplicate image pair: %s, %s" pathA pathB) .
   map (mapSnd ME.Success)

unmatchedImages ::
   [FilePath] -> Map (FilePath, FilePath) a -> [FilePath]
unmatchedImages paths pairs =
   Set.toList $
   Set.difference
      (Set.fromList $ concatMap (\(pathA,pathB) -> [pathA,pathB]) $
         Map.keys pairs)
      (Set.fromList paths)

warnUnmatchedImages :: [FilePath] -> Map (FilePath, FilePath) a -> IO ()
warnUnmatchedImages paths relations = do
   let unmatched = unmatchedImages paths relations
   when (not $ null unmatched) $ IO.hPutStrLn IO.stderr $
      "image in relations but not in the image list: " ++
      List.intercalate ", " unmatched



write :: (Csv.ToNamedRecord a, Csv.DefaultOrdered a) => FilePath -> [a] -> IO ()
write path = BL.writeFile path . Csv.encodeDefaultOrderedByName


writeDisplacement ::
   FilePath -> [(ix, (FilePath, FilePath), Maybe (Float, Float))] -> IO ()
writeDisplacement path =
   write path .
   map
      (\(_, (pathA,pathB), md) ->
         Displacement pathA pathB (Just $ unrelated $ isNothing md) md)

writeRotated ::
   FilePath ->
   [(ix, (FilePath, FilePath), [((Float, Float), (Float, Float))])] -> IO ()
writeRotated path =
   write path .
   concatMap
      (\(_, paths, rots) ->
         Rotated (Just (paths, Just $ unrelated $ null rots)) Nothing
         :
         map (\rot -> Rotated Nothing (Just rot)) rots)



{-
(.:) accepts missing fields and (.:?) instead accepts missing columns.

Warning: This implementation would also accept ill-formated cells:
(.:?) m field = (m.:field) <|> pure Nothing
-}
(.:?) ::
   Csv.FromField a => Csv.NamedRecord -> B.ByteString -> Csv.Parser (Maybe a)
(.:?) m field = traverse Csv.parseField $ HashMap.lookup field m

maybeCorrection ::
   (Csv.FromField a) => a -> Maybe a -> Maybe (Maybe a) -> Maybe a
maybeCorrection zero a mda = fromMaybe (zero <$ a) mda

parseAngle :: Csv.FromField a => Csv.NamedRecord -> Csv.Parser (Maybe a)
parseAngle m = join <$> m .:? angleId

class Csv.DefaultOrdered angleCorr => AngleCorrected angleCorr where
   autoAngleCorrection :: angleCorr
   parseAngles ::
      Csv.NamedRecord -> Csv.Parser (Maybe (Degree Float), angleCorr)

newtype
   AngleCorrection =
      AngleCorrection {getAngleCorrection :: Maybe (Degree Float)}

instance AngleCorrected AngleCorrection where
   autoAngleCorrection = AngleCorrection Nothing
   parseAngles m =
      liftA2
         (\a mda -> (a, AngleCorrection $ maybeCorrection (Degree 0) a mda))
         (parseAngle m) (m .:? dAngleId)

data NoAngleCorrection = NoAngleCorrection

instance AngleCorrected NoAngleCorrection where
   autoAngleCorrection = NoAngleCorrection
   parseAngles m = liftA2 (,) (parseAngle m) (pure NoAngleCorrection)


data
   Proposed angleCorr =
      Proposed FilePath
         (Maybe (Degree Float), angleCorr)
         (Maybe Float, Maybe Float)

propPath :: Proposed angleCorr -> FilePath
propPath (Proposed path _ _) = path

propAngleCorr :: Proposed angleCorr -> angleCorr
propAngleCorr (Proposed _ (_,angleCorr) _) = angleCorr

instance Csv.DefaultOrdered AngleCorrection where
   headerOrder _ = Csv.header [dAngleId]

instance Csv.DefaultOrdered NoAngleCorrection where
   headerOrder _ = Csv.header []

instance
   Csv.DefaultOrdered angleCorr =>
      Csv.DefaultOrdered (Proposed angleCorr) where
   headerOrder p =
      Csv.header [imageId, angleId] <>
      Csv.headerOrder (propAngleCorr p) <>
      Csv.header [xId, yId]

instance
   (AngleCorrected angleCorr) =>
      Csv.FromNamedRecord (Proposed angleCorr) where
   parseNamedRecord m =
      Proposed <$> m .: imageId
         <*> parseAngles m
         <*> liftA2 (,) (join <$> m .:? xId) (join <$> m .:? yId)

read ::
   (Csv.FromNamedRecord a, Csv.DefaultOrdered a) =>
   FilePath -> IO (Vector a)
read path = do
   (headers, body) <-
      either (ioError . userError) return . Csv.decodeByName
         =<< BL.readFile path
   let ignored =
         Set.toList $
         Set.difference
            (Set.fromList $ Fold.toList headers)
            (Set.fromList $ Fold.toList $
             Csv.headerOrder $ Vector.head body)
   when (not $ null ignored) $ IO.hPutStrLn IO.stderr $
      "ignore unknown columns: " ++ List.intercalate ", " (map B.unpack ignored)
   return body


readDisplacement ::
   [FilePath] -> FilePath ->
   IO (Map (FilePath, FilePath) (Maybe Relation, Maybe (Float, Float)))
readDisplacement imagePaths path = do
   relations <-
      ME.switch (ioError . userError) return .
      imagePairMap .
      foldMap
         (\(Displacement pathA pathB rel d) ->
            ((pathA,pathB), (rel,d)) :
            ((pathB,pathA), (rel, mapPair (negate,negate) <$> d)) :
            [])
      =<< read path
   warnUnmatchedImages imagePaths relations
   return relations

readRotated ::
   [FilePath] -> FilePath ->
   IO (Map (FilePath, FilePath)
         (Maybe Relation, [((Float, Float), (Float, Float))]))
readRotated imagePaths path = do
   relationsPlain <- read path
   relations <-
      ME.switch (ioError . userError) return $
      imagePairMap .
      concatMap
         (\((pathA,pathB), (rel,ds)) ->
            ((pathA,pathB), (rel,ds)) :
            ((pathB,pathA), (rel, map swap ds)) :
            [])
      =<<
      segmentRotated (Vector.toList relationsPlain)
   warnUnmatchedImages imagePaths relations
   return relations