packages feed

weekdaze-0.0.0.1: src-lib/WeekDaze/LocationView/Timetable.hs

{-
	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 personal /timetable/ for any week, for each /location/.

	* It contains identical information to 'StudentView.Timetable.Timetable',
	but the data has been re-indexed to present it in a form relevant to a /location/ rather than a /student/.
-}

module WeekDaze.LocationView.Timetable(
-- * Types
-- ** Type-synonyms
	Timetable,
--	Booking,
-- * Functions
-- ** Mutators
--	bookStudentViewLesson,
--	bookLocationViewLesson,
-- ** Translation
	fromStudentViewTimetable,
	toStudentViewTimetable,
	toXHtml
) where

import			Data.Map((!))
import qualified	Data.Array.IArray
import qualified	Data.Foldable
import qualified	Data.Map
import qualified	Data.Maybe
import qualified	Data.Set
import qualified	Text.XHtml.Strict
import qualified	WeekDaze.Aggregate.LocationCatalogue	as Aggregate.LocationCatalogue
import qualified	WeekDaze.Data.Course			as Data.Course
import qualified	WeekDaze.Data.Location			as Data.Location
import qualified	WeekDaze.LocationView.Lesson		as LocationView.Lesson
import qualified	WeekDaze.LocationView.LessonResourceIds	as LocationView.LessonResourceIds
import qualified	WeekDaze.Model.Lesson			as Model.Lesson
import qualified	WeekDaze.Model.Meeting			as Model.Meeting
import qualified	WeekDaze.Model.Timetable		as Model.Timetable
import qualified	WeekDaze.Model.TimetableForWeek		as Model.TimetableForWeek
import qualified	WeekDaze.StudentView.LessonResourceIds	as StudentView.LessonResourceIds
import qualified	WeekDaze.StudentView.Timetable		as StudentView.Timetable
import qualified	WeekDaze.Temporal.Time			as Temporal.Time
import qualified	WeekDaze.Text.CSS			as Text.CSS
import			Text.XHtml.Strict((+++), (<<))
import			WeekDaze.Text.XHTML()

-- | An association-list of the timetables for all /locations/.
type Timetable locationId timeslotId teacherId level	= Model.Timetable.Timetable locationId timeslotId (LocationView.LessonResourceIds.LessonResourceIds teacherId) level

{- |
	* Build a 'Timetable' by inverting a 'StudentView.Timetable.Timetable'.

	* CAVEAT: in the original 'StudentView.Timetable.Timetable', different /student-bodies/ may simultaneously reference a single /location/.
	This is normal behaviour, provided the /location/ has sufficient capacity,
	& is implemented seemlessly, as a /map/-structure with one or more /key/s referencing a /value/, containing the same /location/.

	* When inverting such a /timetable/, this many-to-one relationship becomes a one-to-many, & a single /location-id/ must now reference many /student-bodies/.
	The /map/-structure (cf. /multi-map/), can't directly accommodate multiple identical keys, with different values,
	so this is achieved by merging the /student-bodies/ into a class of /student/s.
-}
fromStudentViewTimetable :: (
	Data.Array.IArray.Ix	timeslotId,
	Enum			timeslotId,
	Eq			level,
	Eq			teacherId,
	Ord			locationId,
	Show			level,
	Show			locationId,
	Show			teacherId,
	Show			timeslotId
 )
	=> Bool							-- ^ Permit a temporary /student-body/ merger.
	-> Timetable locationId timeslotId teacherId level	-- ^ An unallocated /timetable/, supplied for efficiency.
	-> StudentView.Timetable.Timetable timeslotId locationId teacherId level
	-> Timetable locationId timeslotId teacherId level
fromStudentViewTimetable permitTemporaryStudentBodyMerger	= Data.Map.foldrWithKey (
	\studentBody studentViewTimetableForWeek locationViewTimetable	-> snd {-timetable-} $ Data.Foldable.foldr (
		\studentViewTimetableForDay (day, locationViewTimetable')	-> (
			pred day,	-- Because of the use of 'foldr'.
			snd {-timetable-} $ Data.Foldable.foldr (
				\maybeStudentViewLesson (timeslotId, locationViewTimetable'')	-> (
					pred timeslotId,	-- Because of the use of 'foldr'.
					Data.Maybe.maybe locationViewTimetable'' (
						($ locationViewTimetable'') . bookStudentViewLesson permitTemporaryStudentBodyMerger {-partially apply-} . (,) (
							studentBody,
							Temporal.Time.mkTime day timeslotId
						) {-construct a booking-}
					) maybeStudentViewLesson
				)
			) (
				snd {-maximum timeslotId-} $ Data.Array.IArray.bounds studentViewTimetableForDay,
				locationViewTimetable'
			) studentViewTimetableForDay
		)
	) (
		snd {-maximum day-} $ Data.Array.IArray.bounds studentViewTimetableForWeek,
		locationViewTimetable
	) studentViewTimetableForWeek
 )

{- |
	* Build a 'StudentView.Timetable.Timetable', by inverting a 'Timetable'.

	* Each /lesson/ in the /timetable/ seen from the /location/'s perspective, contains a /student-class/.
	This /student-class/ must be broken into /student-bodies/, before inserting repeatedly into the required /timetable/ as seen from the /student-body/'s perpective.
-}
toStudentViewTimetable
	:: (Data.Array.IArray.Ix timeslotId, Enum timeslotId)
	=> StudentView.Timetable.Timetable timeslotId locationId teacherId level	-- ^ An unallocated /timetable/, supplied for efficiency.
	-> Timetable locationId timeslotId teacherId level
	-> StudentView.Timetable.Timetable timeslotId locationId teacherId level
toStudentViewTimetable	= Data.Map.foldrWithKey (
	\locationId locationViewTimetableForWeek studentViewTimetable	-> snd {-timetable-} $ Data.Foldable.foldr (
		\locationViewTimetableForDay (day, studentViewTimetable')	-> (
			pred day,	-- Because of the use of 'foldr'.
			snd {-timetable-} $ Data.Foldable.foldr (
				\maybeLocationViewLesson (timeslotId, studentViewTimetable'')	-> (
					pred timeslotId,	-- Because of the use of 'foldr'.
					Data.Maybe.maybe studentViewTimetable'' (
						($ studentViewTimetable'') . bookLocationViewLesson {-partially apply-} . (,) (
							locationId,
							Temporal.Time.mkTime day timeslotId
						) {-construct a booking-}
					) maybeLocationViewLesson
				)
			) (
				snd {-maximum timeslotId-} $ Data.Array.IArray.bounds locationViewTimetableForDay,
				studentViewTimetable'
			) locationViewTimetableForDay
		)
	) (
		snd {-maximum day-} $ Data.Array.IArray.bounds locationViewTimetableForWeek,
		studentViewTimetable
	) locationViewTimetableForWeek
 )

-- | Make a 'StudentView.Timetable.Booking' in a /LocationView-timetable/.
bookStudentViewLesson :: (
	Data.Array.IArray.Ix	timeslotId,
	Eq			level,
	Eq			teacherId,
	Ord			locationId,
	Show			level,
	Show			locationId,
	Show			teacherId,
	Show			timeslotId
 )
	=> Bool									-- ^ Permit a temporary /student-body/ merger.
	-> StudentView.Timetable.Booking timeslotId locationId teacherId level	-- ^ The /coordinates/ & /lesson/ from the perspective of the /student/-view of the /timetable/.
	-> Timetable locationId timeslotId teacherId level			-- ^ The /timetable/ as seen from the /location/'s perspective.
	-> Timetable locationId timeslotId teacherId level
bookStudentViewLesson permitTemporaryStudentBodyMerger ((studentBody, time), studentViewLesson) locationViewTimetable	= Model.Timetable.defineTimeslot (
	locationViewTimetableCoordinates,
	Just studentViewLesson {
		Model.Lesson.getResourceIds	= LocationView.LessonResourceIds.mkLessonResourceIds (
			Data.Set.insert studentBody $ case Model.Timetable.getMaybeLesson locationViewTimetableCoordinates locationViewTimetable of
				Nothing	-> Data.Set.empty	-- Start a new student-class.
				Just locationViewLesson
					| and [
						permitTemporaryStudentBodyMerger,
						LocationView.LessonResourceIds.getTeacherId locationViewResourceIds == teacherId,
						Model.Lesson.getSubject locationViewLesson == Model.Lesson.getSubject studentViewLesson
					]		-> LocationView.LessonResourceIds.getStudentClass locationViewResourceIds
					| otherwise	-> error $ "WeekDaze.LocationView.Timetable.bookStudentViewLesson:\tlesson at coordinates=" ++ show locationViewTimetableCoordinates ++ " has already been booked in either a different subject or location; " ++ show (locationViewLesson, studentViewLesson) ++ "."
					where
						locationViewResourceIds	= Model.Lesson.getResourceIds locationViewLesson
		 ) teacherId
	} -- Mutate.
 ) locationViewTimetable where
	studentViewResourceIds			= Model.Lesson.getResourceIds studentViewLesson
	teacherId				= StudentView.LessonResourceIds.getTeacherId studentViewResourceIds
	locationViewTimetableCoordinates	= (StudentView.LessonResourceIds.getLocationId studentViewResourceIds, time)

-- | A /lesson/ qualified by the /coordinates/ at which it is booked.
type Booking locationId timeslotId teacherId level	= Model.Timetable.Booking locationId timeslotId (LocationView.LessonResourceIds.LessonResourceIds teacherId) level

-- | Books a /LocationView-lesson/ in a /StudentView-timetable/.
bookLocationViewLesson
	:: Data.Array.IArray.Ix timeslotId
	=> Booking locationId timeslotId teacherId level				-- ^ The /coordinates/ & /lesson/ from the perspective of the /location/-view of the /timetable/.
	-> StudentView.Timetable.Timetable timeslotId locationId teacherId level	-- ^ The /timetable/ as seen from the /student/'s perspective.
	-> StudentView.Timetable.Timetable timeslotId locationId teacherId level
bookLocationViewLesson ((locationId, time), locationViewLesson) studentViewTimetable	= Data.Set.foldr (
	Model.Timetable.defineTimeslot . flip (,) (
		Just $ LocationView.Lesson.toStudentView locationId locationViewLesson
	) . flip (,) time
 ) studentViewTimetable $ LocationView.LessonResourceIds.getStudentClass locationViewResourceIds where
	locationViewResourceIds	= Model.Lesson.getResourceIds locationViewLesson

-- | Render in /XHTML/, as a /definition-list/.
toXHtml :: (
	Data.Array.IArray.Ix	timeslotId,
	Fractional		minimumContrastRatio,
	Ord			level,
	Ord			locationId,
	Ord			minimumContrastRatio,
	Ord			teacherId,
	Show			level,
	Show			teacherId,
	Text.XHtml.Strict.HTML	campus,
	Text.XHtml.Strict.HTML	level,
	Text.XHtml.Strict.HTML	locationId,
	Text.XHtml.Strict.HTML	synchronisationId,
	Text.XHtml.Strict.HTML	teacherId,
	Text.XHtml.Strict.HTML	timeslotId
 )
	=> (locationId -> LocationView.Lesson.Lesson teacherId level -> Data.Course.Course synchronisationId level timeslotId)	-- ^ Find the /course/ to which the specified /lesson/ belongs.
	-> Aggregate.LocationCatalogue.LocationCatalogue locationId campus
	-> Model.Timetable.GenericTimetableToMarkup locationId minimumContrastRatio teacherId timeslotId (Timetable locationId timeslotId teacherId level)
toXHtml findCourseForLocationViewLesson locationCatalogue displaySupplementaryInformation meetingsByTime htmlMergeDuplicateTimeslots displayAxisLabels weekend maybeGenerateLessonColourFrom	= Text.XHtml.Strict.defList . map (
	\(locationId, timetableForWeek)	-> let
		locationProfile			= locationCatalogue ! locationId
		nUnallocatedAvailableTimeslots	= Model.TimetableForWeek.countUnallocatedAvailableTimeslots locationProfile timetableForWeek
	in (
		Text.XHtml.Strict.unordList (
			(
				Data.Location.getCampus locationProfile +++ ' ' +++ locationId
			) : if displaySupplementaryInformation
				then Data.Maybe.catMaybes [
					Just $ Text.XHtml.Strict.thespan Text.XHtml.Strict.! [
						Text.XHtml.Strict.theclass Text.CSS.infoCSSIdentifier,
						Text.XHtml.Strict.title "The total capacity to accommodate students at this location."
					] << (
						"Capacity" +++ Text.XHtml.Strict.spaceHtml +++ '=' +++ Text.XHtml.Strict.spaceHtml +++ Text.XHtml.Strict.thespan Text.XHtml.Strict.! [
							Text.XHtml.Strict.theclass Text.CSS.numericDataCSSIdentifier
						] << Data.Location.getCapacity (locationCatalogue ! locationId)
					),
					if nUnallocatedAvailableTimeslots == 0
						then Nothing
						else Just $ Text.XHtml.Strict.thespan Text.XHtml.Strict.! [
							Text.XHtml.Strict.theclass Text.CSS.infoCSSIdentifier,
							Text.XHtml.Strict.title "Available but unallocated time-slots, at this location."
						] << (
							"Unallocated time-slots" +++ Text.XHtml.Strict.spaceHtml +++ '=' +++ Text.XHtml.Strict.spaceHtml +++ Text.XHtml.Strict.thespan Text.XHtml.Strict.! [
								Text.XHtml.Strict.theclass Text.CSS.numericDataCSSIdentifier
							] << nUnallocatedAvailableTimeslots
						),
					let
						facilityNames	= Data.Location.getFacilityNames locationProfile
					in if Data.Set.null facilityNames
						then Nothing
						else Just $ Text.XHtml.Strict.unordList (map show $ Data.Set.toList facilityNames) Text.XHtml.Strict.! [
							Text.XHtml.Strict.theclass Text.CSS.infoCSSIdentifier,
							Text.XHtml.Strict.title "Facilities available at this location."
						]
				]
				else []
		) Text.XHtml.Strict.! [Text.XHtml.Strict.theclass Text.CSS.observerSummaryCSSIdentifier],	-- CAVEAT: xhtml only permits inline elements to be nested inside a 'dt' element, & 'ul' is a block element.
		Model.TimetableForWeek.toXHtml (
			findCourseForLocationViewLesson locationId	-- Partially apply.
		) locationProfile (
			Model.Meeting.deleteLocationId locationId meetingsByTime
		) htmlMergeDuplicateTimeslots displayAxisLabels weekend maybeGenerateLessonColourFrom timetableForWeek
	)
 ) . Data.Map.toList