packages feed

pictikz-1.0.0.1: src/Pictikz/Loader.hs

--  Copyright 2017 Marcelo Garlet Millani
--  This file is part of pictikz.

--  pictikz is free software: you can redistribute it and/or modify
--  it under the terms of the GNU General Public License as published by
--  the Free Software Foundation, either version 3 of the License, or
--  (at your option) any later version.

--  pictikz is distributed in the hope that it will be useful,
--  but WITHOUT ANY WARRANTY; without even the implied warranty of
--  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
--  GNU General Public License for more details.

--  You should have received a copy of the GNU General Public License
--  along with pictikz.  If not, see <http://www.gnu.org/licenses/>.

module Pictikz.Loader where

import Data.Matrix hiding (trace)
import Pictikz.Geometry
import Pictikz.Drawing
import Pictikz.Parser
import qualified Pictikz.Graph as G
import Data.List
import Data.Char
import Data.Maybe
import qualified Debug.Trace as D (trace)

import qualified Text.XML.Light as X

data Element a b = Object (Shape a) String String | Line a a a a [b] | Text a a String deriving (Show, Eq)

isObject (Object _ _ _) = True
isObject _ = False
isLine (Line _ _ _ _ _) = True
isLine _ = False
isText (Text _ _ _) = True
isText _ = False

lineStyle (Line _ _ _ _ style) = style

defaultText = Text 0 0 "ERROR"
defaultRectangle = (Rectangle 0 0 0 0, "", "", identity 3)
defaultEllipsis  = (Ellipsis  0 0 0 0, "", "", identity 3)
defaultGNode = G.Node 0 0 "" ""

loadGraph svg =
  let contents = X.parseXML svg
      elements = concatMap (parseElements (identity 3)) contents
      gnames = filter isText elements
      gnodes = assignNames (filter isObject elements) gnames
      gedges = map (closest gnodes) $ fixEdgeStyle $ filter isLine elements
  in G.Graph (map (fPos (\(x,y) -> (x,-y))) (map toNode gnodes)) gedges
  where
    parseElements matrix (X.Elem element)
      -- Objects
      | (X.qName $ X.elName element) `elem` ["rect"] =
        let (shape, id, name, m2) = foldl parseRectangle defaultRectangle $ X.elAttribs element
        in [transform (matrix * m2) (Object shape id name)]
      | (X.qName $ X.elName element) `elem` ["ellipse", "circle"] =
        let (shape, id, name, m2) = foldl parseEllipsis defaultEllipsis $ X.elAttribs element
        in [transform (matrix * m2) (Object shape id name)]
      -- Transformations
      | (X.qName $ X.elName element) `elem` ["defs"] = []
      | (X.qName $ X.elName element) `elem` ["g"] =
        let matrix' = foldl parseG matrix $ X.elAttribs element
        in concatMap (parseElements (matrix * matrix') ) $ X.elContent element
      -- Edges
      | (X.qName $ X.elName element) == "path" =  [transform matrix $ foldl parseEdge (Line 0 0 0 0 []) $ X.elAttribs element]
      -- Text
      | (X.qName $ X.elName element) == "text" =  [transform matrix $ parseName $ X.elContent element]
      | otherwise = concatMap (parseElements matrix ) $ X.elContent element
    parseElements matrix _ = []
    transform matrix ( Object (Rectangle x y w h) id name) =
      let [x', y',_] = toList $ matrix * (fromList 3 1 [x,y,1])
          [w1,h1,w2,h2,_,_] = toList $ matrix * (fromList 3 2 [w,0,0,h,0,0])
          w' = maximum [w1,h1,0] - minimum [w1,h1,0]
          h' = maximum [w2,h2,0] - minimum [w2,h2,0]
      in (Object (Rectangle x' y' w' h') id name)
    transform matrix (Object (Ellipsis x y rx ry) id name) =
      let [x', y',_] = toList $ matrix * (fromList 3 1 [x,y,1])
          [rx1,ry1,rx2,ry2,_,_] = toList $ matrix * (fromList 3 2 [rx,0,0,ry,0,0])
          rx' = maximum [rx1,ry1,0] - minimum [rx1,ry1,0]
          ry' = maximum [rx2,ry2,0] - minimum [rx2,ry2,0]
      in (Object (Ellipsis x' y' rx' ry') id name)
    transform matrix (Line x0 y0 x1 y1 a) =
      let [x0', y0',_, x1', y1', _] = toList $ matrix * (fromList 3 2 [x0,y0,1, x1,y1,1])
      in Line x0' y0' x1' y1' a
    transform matrix (Text x y str) =
      let [x', y',_] = toList $ matrix * (fromList 3 1 [x,y,1])
      in (Text x' y' str)
    parseG matrix attr
      | "transform" == (X.qName $ X.attrKey attr) = matrix * (parseTransform $ X.attrVal attr)
      | otherwise = matrix
    parseRectangle (Rectangle x y w h, id, name, matrix) attr
      | "id" == (X.qName $ X.attrKey attr) = (Rectangle x y w h, (X.attrVal attr), name, matrix)
      | "x"  == (X.qName $ X.attrKey attr) = (Rectangle (read $ X.attrVal attr :: Float) y w h, id, name, matrix)
      | "y"  == (X.qName $ X.attrKey attr) = (Rectangle x (read $ X.attrVal attr :: Float) w h, id, name, matrix)
      | "height" == (X.qName $ X.attrKey attr) = (Rectangle x y w (read $ X.attrVal attr :: Float), id, name, matrix)
      | "width"  == (X.qName $ X.attrKey attr) = (Rectangle x y (read $ X.attrVal attr :: Float) h, id, name, matrix)
      | "transform"  == (X.qName $ X.attrKey attr) = (Rectangle x y (read $ X.attrVal attr :: Float) h, id, name, matrix * (parseTransform $ X.attrVal attr))
      | otherwise = (Rectangle x y w h, id, name, matrix)
    parseEllipsis (Ellipsis x y rx ry, id, name, matrix) attr
      | "id" == (X.qName $ X.attrKey attr) = (Ellipsis x y rx ry, (X.attrVal attr), name, matrix)
      | "cx" == (X.qName $ X.attrKey attr) = (Ellipsis (read $ X.attrVal attr :: Float) y rx ry, id, name, matrix)
      | "cy" == (X.qName $ X.attrKey attr) = (Ellipsis x (read $ X.attrVal attr :: Float) rx ry, id, name, matrix)
      | "rx" == (X.qName $ X.attrKey attr) = (Ellipsis x y (read $ X.attrVal attr :: Float)  ry, id, name, matrix)
      | "ry" == (X.qName $ X.attrKey attr) = (Ellipsis x y rx (read $ X.attrVal attr :: Float) , id, name, matrix)
      | "r"  == (X.qName $ X.attrKey attr) = (Ellipsis x y (read $ X.attrVal attr :: Float) (read $ X.attrVal attr :: Float), id, name, matrix)
      | "transform"  == (X.qName $ X.attrKey attr) = (Ellipsis x y rx ry, id, name, matrix * (parseTransform $ X.attrVal attr))
      | otherwise = (Ellipsis x y rx ry, id, name, matrix)
    toNode (Object (Rectangle x y w h) id name) = G.Node (x + w/2) (y + h/2) id name [G.Rectangle]
    toNode (Object (Ellipsis x y _ _)  id name) = G.Node x y id name []
    parseName [] = defaultText
    parseName ((X.Elem element):xs)
      | (X.qName $ X.elName element) == "tspan" =
        let (x,y, matrix) = foldl parseCoordinates (0,0, identity 3) $ X.elAttribs element
            getName (X.Text cdata) = Left $ X.cdData cdata
            getName _ = Right ""
            name  = foldl (>>) (Right "") $ map getName $ X.elContent element
        in case name of
          Left  n -> Text x y n
          Right _ -> defaultText
      | otherwise = parseName xs
    parseEdge (Line xa ya xb yb a) attr
      | "d" == (X.qName $ X.attrKey attr) =
        let path = parsePath $ X.attrVal attr
            ((x0,y0), (x1,y1)) = ((head path :: (Float, Float)), (last path :: (Float, Float)))
        in Line x0 y0 x1 y1 a
      | "style"        == (X.qName $ X.attrKey attr) =
        let fields = parseStyle $ X.attrVal attr
            style = edgeStyle fields
        in Line xa ya xb yb style
      | otherwise = Line xa ya xb yb a
    edgeStyle :: [(String, String)] -> [(G.EdgeStyle, Float)]
    edgeStyle [] = []
    edgeStyle (x:xs) = case x of
      ("marker-end", "none")        -> (G.Arrow G.ArrowNone, undefined) : edgeStyle xs
      ("marker-end", _)             -> (G.Arrow G.ArrowTo,   undefined) : edgeStyle xs
      ("marker-start", "none")      -> (G.Arrow G.ArrowNone, undefined) : edgeStyle xs
      ("marker-start", _)           -> (G.Arrow G.ArrowFrom, undefined) : edgeStyle xs
      ("stroke-width", len)         -> (G.Thick, (readLength len)) : edgeStyle xs
      ("stroke-dasharray", "none")  -> edgeStyle xs
      ("stroke-dasharray", dashes)  -> let dash = takeWhile (/=',') dashes in (G.Dashed, (read dash :: Float)) : edgeStyle xs
      _ -> edgeStyle xs
    fixEdgeStyle ls =
      let strokeWs = map snd $ filter (\(s,v) -> s == G.Thick) $ concatMap lineStyle ls
          minStroke = minimum strokeWs
          maxStroke = maximum strokeWs
          midStroke = minStroke + (maxStroke - minStroke) / 2
          fixLine arrow [] = [G.Arrow arrow]
          fixLine arrow (s:ss) = case s of
            (G.Thick, v)   -> if v > midStroke && v > minStroke * 1.4 then G.Thick : fixLine arrow ss else (fixLine arrow ss)
            (G.Dashed, v)  -> (if v > 2*minStroke then G.Dashed else G.Dotted) : (fixLine arrow ss)
            (G.Arrow a, _) -> fixLine (G.joinArrow a arrow) ss
      in map (\(Line x0 y0 x1 y1 s) -> Line x0 y0 x1 y1 (fixLine G.ArrowNone s)) ls
    parseCoordinates (x,y, matrix) attr
      | "x" == (X.qName $ X.attrKey attr) = ((read $ X.attrVal attr :: Float), y, matrix)
      | "y" == (X.qName $ X.attrKey attr) = (x,(read $ X.attrVal attr :: Float), matrix)
      | "transform"  == (X.qName $ X.attrKey attr) = (x,y, matrix * (parseTransform $ X.attrVal attr))
      | otherwise = (x,y, matrix)
    assignNames [] _ = []
    assignNames gnodes [] = gnodes
    assignNames gnodes (t:ts) =
      let (Object s iD n) = assignName gnodes t
      in (Object s iD n) : assignNames (filter (\(Object _ iD1 _) -> iD1 /= iD) gnodes) ts
    assignName gnodes (Text x0 y0 n) =
      let dist s = squareDistance (x0,y0) s
          (Object s iD _ ) = minimumBy (\(Object s0 _ _) (Object s1 _ _) -> compare (dist s0) (dist s1) ) gnodes
      in (Object s iD n )
    closest vertices (Line x0 y0 x1 y1 a) =
      let p0 = (x0, y0)
          p1 = (x1, y1)
          (n0,_) = minimumBy (\p q -> compare (snd p) (snd q)) $ map (\(Object shape id _) -> (id, squareDistance p0 shape)) vertices
          (n1,_) = minimumBy (\p q -> compare (snd p) (snd q)) $ map (\(Object shape id _) -> (id, squareDistance p1 shape)) vertices
      in G.Edge n0 n1 a