packages feed

casa-reoc-0.1.0.0: src/ReOC/Render/OperationsManual/Diagram.hs

{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -Wall #-}

{- | The organisational diagram of the operations manual (Section 1.1.3), drawn
from the key positions: the CEO, the positions that report to the CEO, and
the remote pilots, who report to the chief remote pilot. Each position shows
its holder.
-}
module ReOC.Render.OperationsManual.Diagram (
  organisationalDiagramSvg,
  organisationalDiagramDescription,
  organisationalDiagramFrame,
) where

import Data.Text (Text)
import qualified Data.Text as Text
import Data.Text.Encoding (encodeUtf8)
import ReOC.Model
import ReOC.Personnel (keyPositions, soleHolder)
import ReOC.Render.Base64 (base64)
import ReOC.Render.Text (escapeXml, get, listing)

-- A box of the diagram: its centre, its title and its holder.
type Box = (Int, Int, Text, Text)

boxWidth, boxHeight, fontSize :: Int
boxWidth = 420
boxHeight = 100
fontSize = 25

-- The boxes and lines of the diagram, in units of 0.1 mm.
layout :: ReOC -> ([Box], [(Int, Int, Int, Int)])
layout r =
  let ps = keyPositions r
      name :: Nominee -> Text
      name n = formTextText (get (person . fullName) n)
      title a t = Text.toUpper (Text.take 1 t) <> Text.drop 1 t <> " (" <> a <> ")"
      reports = [(a, t, n) | (a, t, n) <- ps, a /= "CEO"]
      count = length reports
      spacing = 1320 `div` max 1 count
      centres = [spacing * k + spacing `div` 2 | k <- [0 .. count - 1]]
      ceo = [(660, 70, title a t, name n) | (a, t, n) <- ps, a == "CEO"]
      second = [(x, 290, title a t, name n) | (x, (a, t, n)) <- zip centres reports]
      crpX = case [x | (x, (a, _, _)) <- zip centres reports, a == "CRP"] of
        x : _ -> x
        [] -> 660
      rpHolder = case get (operationsManual . pilots) r of
        CRPIsOnlyRemotePilot -> "The CRP"
        OtherRemotePilots -> "The CRP and engaged RPs"
      rp = [(crpX, 490, "Remote pilot (RP)", rpHolder)]
      half = boxHeight `div` 2
      minX = foldr min 660 centres
      maxX = foldr max 660 centres
      ls =
        [(660, 70 + half, 660, 190), (minX, 190, maxX, 190)]
          <> [(x, 190, x, 290 - half) | x <- centres]
          <> [(crpX, 290 + half, crpX, 490 - half)]
   in (ceo <> second <> rp, ls)

-- An estimate of the width of text in the diagram's font, from the widths of the form's widest font.
estimatedWidth :: Text -> Int
estimatedWidth t =
  round (maybe (fromIntegral (Text.length t) * 0.6) (* 0.1) (textWidth Wide t) * fromIntegral fontSize :: Double)

-- | The diagram, as SVG.
organisationalDiagramSvg :: ReOC -> Text
organisationalDiagramSvg r =
  let (boxes, ls) = layout r
      n = Text.pack . show
      line (x1, y1, x2, y2) =
        "<line x1=\"" <> n x1 <> "\" y1=\"" <> n y1 <> "\" x2=\"" <> n x2 <> "\" y2=\"" <> n y2 <> "\" stroke=\"#1f3864\" stroke-width=\"4\"/>"
      textAt x y weight t =
        let inner = boxWidth - 24
            widthOf = estimatedWidth t * (if weight == "bold" then 11 else 10) `div` 10
            fit = if widthOf > inner then " textLength=\"" <> n inner <> "\" lengthAdjust=\"spacingAndGlyphs\"" else ""
         in "<text x=\""
              <> n x
              <> "\" y=\""
              <> n y
              <> "\" text-anchor=\"middle\" font-family=\"Liberation Sans, Arial, Helvetica, sans-serif\" font-size=\""
              <> n fontSize
              <> "\" font-weight=\""
              <> weight
              <> "\""
              <> fit
              <> ">"
              <> escapeXml t
              <> "</text>"
      box (x, y, t, h) =
        "<rect x=\""
          <> n (x - boxWidth `div` 2)
          <> "\" y=\""
          <> n (y - boxHeight `div` 2)
          <> "\" width=\""
          <> n boxWidth
          <> "\" height=\""
          <> n boxHeight
          <> "\" rx=\"10\" fill=\"#ffffff\" stroke=\"#1f3864\" stroke-width=\"4\"/>"
          <> textAt x (y - 8) "bold" t
          <> textAt x (y + 28) "normal" h
   in "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
        <> "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"132mm\" height=\"56mm\" viewBox=\"0 0 1320 560\">"
        <> Text.concat (map line ls)
        <> Text.concat (map box boxes)
        <> "</svg>"

-- | A description of the diagram, for readers who cannot see it.
organisationalDiagramDescription :: ReOC -> Text
organisationalDiagramDescription r =
  let ps = keyPositions r
      reports = [t | (a, t, _) <- ps, a /= "CEO"]
      held =
        case soleHolder r of
          Just _ -> "All positions are held by one person."
          Nothing -> Text.unwords [formTextText (get (person . fullName) n) <> " holds the position of " <> t <> "." | (_, t, n) <- ps]
   in "Organisational diagram: CEO; " <> listing reports <> " report to the CEO; remote pilots report to the chief remote pilot. " <> held

-- | The diagram as a frame of Flat ODF text, with the SVG embedded.
organisationalDiagramFrame :: ReOC -> Text
organisationalDiagramFrame r =
  "<draw:frame text:anchor-type=\"as-char\" draw:z-index=\"0\" draw:name=\"Organisational diagram\" draw:style-name=\"gr2\" draw:text-style-name=\"P149\" svg:width=\"13.2cm\" svg:height=\"5.6cm\">"
    <> "<draw:image draw:mime-type=\"image/svg+xml\"><office:binary-data>"
    <> base64 (encodeUtf8 (organisationalDiagramSvg r))
    <> "</office:binary-data><text:p/></draw:image>"
    <> "<svg:desc>"
    <> escapeXml (organisationalDiagramDescription r)
    <> "</svg:desc></draw:frame>"