weekdaze-0.0.0.1: src-lib/WeekDaze/Data/Location.hs
{-# LANGUAGE CPP #-}
{-
Copyright (C) 2013-2015 Dr. Alistair Ward
This file is part of WeekDaze.
WeekDaze 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.
WeekDaze 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 WeekDaze. If not, see <http://www.gnu.org/licenses/>.
-}
{- |
[@AUTHOR@] Dr. Alistair Ward
[@DESCRIPTION@]
* Defines the attributes of a /location/, in terms of its; /capacity/, /facilities/, whether it's /off-site/, & its /availability/.
* It refers to a more general concept than a merely a /class-room/, including for example, an assembly-hall or a games-field.
-}
module WeekDaze.Data.Location(
-- * Types
-- ** Type-synonyms
FacilityName,
FacilityNames,
WastedResources,
Locus,
-- ** Data-types
Profile(
-- MkProfile,
getCapacity,
getFacilityNames,
getAvailability,
getCampus
),
-- * Constants
-- tag,
capacityTag,
-- facilitiesTag,
facilityTypeIdTag,
facilityNameTag,
facilityValueTag,
-- facilityTypeTableName,
defaultFacilityNames,
-- * Functions
calculateWaste,
calculateRelativeWaste,
calculateRelativeUse,
#ifdef USE_HDBC
findFacilityNameByFacilityTypeId,
#endif
-- ** Accessors
-- getWastedCapacity,
getWastedFacilityNames,
-- ** Constructor
mkProfile,
-- ** Predicates
hasFacilities,
isSuitable
) where
import Control.Arrow((&&&), (***))
import qualified Control.DeepSeq
import qualified Data.Default
import qualified Data.Set
import Data.Set((\\))
import qualified Text.XML.HXT.Arrow.Pickle as HXT
import qualified Text.XML.HXT.DOM.Util
import qualified ToolShed.SelfValidate
import qualified WeekDaze.Data.Resource as Data.Resource
import qualified WeekDaze.Size as Size
import qualified WeekDaze.Temporal.Availability as Temporal.Availability
#ifdef USE_HDBC
import qualified Database.HDBC
import qualified Data.IntMap
import qualified WeekDaze.Database.Selector as Database.Selector
-- | Used to qualify XML.
facilityTypeTableName :: Database.Selector.TableName
facilityTypeTableName = Database.Selector.tablePrefix ++ "facilityType"
-- | Queries the database to build a map of /facilityName/s by /facilityTypeId/.
findFacilityNameByFacilityTypeId
:: Database.HDBC.IConnection connection
=> connection
-> Database.HDBC.SqlValue -- ^ ProjectId
-> IO (Data.IntMap.IntMap FacilityName)
findFacilityNameByFacilityTypeId connection projectIdSql = (
Data.IntMap.fromList . map (
\row -> case row of
[facilityTypeIdSql, facilityNameSql] -> (
either (
error . showString "WeekDaze.Data.Location.findFacilityNameByFacilityTypeId:\tfailed to parse the value for " . shows facilityTypeIdTag . showString " read from the database; " . show
) id $ Database.HDBC.safeFromSql facilityTypeIdSql,
Database.HDBC.fromSql facilityNameSql
) -- Pair.
_ -> error $ "WeekDaze.Data.Location.findFacilityNameByFacilityTypeId:\tunexpected number of columns=" ++ show (length row) ++ " in row of table " ++ show facilityTypeTableName ++ "."
)
) `fmap` Database.Selector.select connection [facilityTypeIdTag, facilityNameTag] [facilityTypeTableName] [(Database.Selector.projectIdColumnName, projectIdSql)]
#endif /* USE_HDBC */
-- | Used to qualify XML.
tag :: String
tag = "locationProfile"
-- | Used to qualify SQL & XML.
capacityTag :: String
capacityTag = "capacity"
-- | Used to qualify XML.
facilitiesTag :: String
facilitiesTag = "facilities"
-- | Used to qualify SQL & XML.
facilityTypeIdTag :: String
facilityTypeIdTag = "facilityTypeId"
-- | Used to qualify SQL & XML.
facilityNameTag :: String
facilityNameTag = "facilityName"
-- | Used to qualify XML.
facilityValueTag :: String
facilityValueTag = "value"
{- |
* A /subject/-specific /facility/, e.g. "desks", "musical instruments", or "goal-posts".
* The specific string used to designate these must match those defined for /course/s.
-}
type FacilityName = String
-- | The set of facility-names advertised by a /location/.
type FacilityNames = Data.Set.Set FacilityName
-- | The default value for /facilityNames/.
defaultFacilityNames :: FacilityNames
defaultFacilityNames = Data.Set.empty
-- | Aggregates the attributes of a /location/, which a generalisation of a room, since it also includes games-fields.
data Profile campus = MkProfile {
getCapacity :: Size.NStudents, -- ^ Typically the number of seats. N.B.: though the games-field has a rather ill-defined /capacity/, the changing-rooms don't.
getFacilityNames :: FacilityNames, -- ^ The /location/ may advertise /facilities/ to support specific activities.
getAvailability :: Temporal.Availability.Availability, -- ^ The /location/ may have a periodic /availability/.
getCampus :: campus -- ^ Proximate /locations/ are grouped into /campus/es, between which intra-day migration is trivial.
} deriving (Eq, Show)
instance Show campus => ToolShed.SelfValidate.SelfValidator (Profile campus) where
getErrors profile@MkProfile {
getAvailability = availability
}
| not $ ToolShed.SelfValidate.isValid availability = ToolShed.SelfValidate.getErrors availability
| otherwise = ToolShed.SelfValidate.extractErrors [
(getCapacity profile <= 0, show capacityTag ++ " must exceed zero; " ++ show profile)
]
instance Data.Resource.Resource (Profile campus) where
getAvailability = getAvailability
instance (
Data.Default.Default campus,
Eq campus,
HXT.XmlPickler campus,
Show campus
) => HXT.XmlPickler (Profile campus) where
xpickle = HXT.xpElem tag . HXT.xpWrap (
Text.XML.HXT.DOM.Util.uncurry4 mkProfile, -- Construct from a quadruple.
\MkProfile {
getCapacity = capacity,
getFacilityNames = facilityNames,
getAvailability = availability,
getCampus = campus
} -> (
capacity,
facilityNames,
availability,
campus
) -- Deconstruct to a quadruple.
) $ HXT.xp4Tuple (
HXT.xpAttr capacityTag HXT.xpInt
) (
HXT.xpDefault defaultFacilityNames . HXT.xpElem facilitiesTag . HXT.xpWrap (
Data.Set.fromList, -- Construct from a List.
Data.Set.toList -- Deconstruct to a List.
) . HXT.xpList1 {-the default is null-} . HXT.xpElem facilityNameTag $ HXT.xpTextAttr facilityValueTag {-can't be null-}
) (
HXT.xpDefault Data.Default.def HXT.xpickle -- Availability.
) (
HXT.xpDefault Data.Default.def HXT.xpickle -- Campus.
)
instance Control.DeepSeq.NFData campus => Control.DeepSeq.NFData (Profile campus) where
rnf (MkProfile x0 x1 x2 x3) = Control.DeepSeq.rnf (x0, x1, x2, x3)
-- | Smart constructor.
mkProfile
:: Show campus
=> Size.NStudents
-> FacilityNames
-> Temporal.Availability.Availability
-> campus
-> Profile campus
mkProfile capacity facilityNames availability campus
| ToolShed.SelfValidate.isValid profile = profile
| otherwise = error $ "WeekDaze.Data.Location.mkProfile:\t" ++ ToolShed.SelfValidate.getFirstError profile ++ "."
where
profile = MkProfile capacity facilityNames availability campus
{- |
* True if the 'Profile' meets or exceeds the specified criteria.
* Takes no account of either /availability/ or /campus/.
-}
isSuitable
:: Size.NStudents
-> FacilityNames
-> Profile campus
-> Bool
isSuitable requiredCapacity requiredFacilityNames profile = requiredCapacity <= getCapacity profile && requiredFacilityNames `Data.Set.isSubsetOf` getFacilityNames profile
-- | True if the /location/ has some facilities.
hasFacilities :: Profile campus -> Bool
hasFacilities = not . Data.Set.null . getFacilityNames
-- | A measure of the /capacity/ & specific /facilities/, which are available at a /location/, but are unused in some context.
type WastedResources = (Size.NStudents, FacilityNames)
-- | Accessor.
getWastedCapacity :: WastedResources -> Size.NStudents
getWastedCapacity = fst
-- | Accessor.
getWastedFacilityNames :: WastedResources -> FacilityNames
getWastedFacilityNames = snd
{- |
* Measures the excess /capacity/ & unused /facilities/, of the specified /location/.
* All /facilities/ are considered equal.
* CAVEAT: each /location/ must have previously passed 'isSuitable'.
-}
calculateWaste
:: Size.NStudents -- ^ The required /capacity/.
-> FacilityNames -- ^ The required /facilities/.
-> Profile campus -- ^ The /location/ whose /resources/ are to be compared with requirements.
-> WastedResources
calculateWaste requiredCapacity requiredFacilities = subtract requiredCapacity . getCapacity &&& (\\ requiredFacilities) . getFacilityNames
-- | Convert 'WastedResources' to a pair of ratios.
calculateRelativeWaste :: Fractional relativeWaste => Profile campus -> WastedResources -> (relativeWaste, relativeWaste)
calculateRelativeWaste profile wastedResources = (
fromIntegral (getWastedCapacity wastedResources) / fromIntegral (getCapacity profile),
let
nFacilities = Data.Set.size $ getFacilityNames profile
in if nFacilities == 0
then 0 -- If the location hasn't any facilities, none can be wasted.
else fromIntegral (Data.Set.size $ getWastedFacilityNames wastedResources) / fromIntegral nFacilities
) -- Pair.
-- | The relative extent to which /capacity/ & /facilities/ are used.
calculateRelativeUse :: Fractional relativeUse => Profile campus -> WastedResources -> (relativeUse, relativeUse)
calculateRelativeUse profile = ((1 -) *** (1 -)) . calculateRelativeWaste profile
-- | A collection of distinct /location-ids/.
type Locus locationId = Data.Set.Set locationId