Created
May 4, 2018 06:01
-
-
Save cry/d93aecbe8e984bf75b914d54dbc199bb to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//Bojangles client side program | |
//Initializing canvas and setting canvas variables | |
var canvas = new fabric.Canvas('timetable', {selection: false}); | |
//Non stock fabric functionality, added in my customized version | |
canvas.pixelRatio = window.devicePixelRatio; | |
var width = 0; | |
var rows = 9; | |
var cols = 5; | |
var height = 0; | |
var widthScale = 0; | |
var heightScale = 0; | |
var yOffset = 16*canvas.pixelRatio; | |
var xOffset = 16*canvas.pixelRatio; | |
var startHour = 9; | |
var tempBlocks = []; | |
var generatedTimetables = []; | |
var courses = []; | |
var depObjects = []; | |
var currDisplayed = 0; | |
var textScale = 1; | |
canvas.renderOnAddRemove = false; | |
canvas.allowTouchScrolling = true; | |
canvas.backgroundColor = '#fff'; | |
canvas.imageSmoothingEnabled = false; | |
var sortOrder = new Sortable(document.getElementById('sortable')); | |
$.getJSON('/data/bojangles/course_names.json', function(jsonIn){ | |
$('#course-inpt').typeahead({source: jsonIn}); | |
}); | |
//Fetches a course from the server and adds it to the course array | |
var addSubject = function(courseName){ | |
if(courses.length >= 6) return; | |
var contains = false; | |
var courseCode = courseName.split(' - ')[0]; | |
for(var i = 0; !contains && i < courses.length; ++i){ | |
if(courses[i].course_code === courseCode) contains = true; | |
} | |
if(!contains){ | |
$.getJSON('/data/bojangles/'+ courseCode.toUpperCase() + | |
'.json', function(jsonIn){ | |
courses.push(jsonIn); | |
$('#chosen').append('<li class="list-group-item class-inpt">' + | |
jsonIn.course_code + ' - ' + jsonIn.course_name + | |
'<button class="btn btn-danger btn-xs pull-right rm-course">'+ | |
'Remove</btn></li>'); | |
if(jsonIn.warnings){ | |
throwWarning(jsonIn.warnings, false); | |
} | |
}); | |
} | |
}; | |
//Fills the canvas with the timetable labels and grid | |
var createTableGrid = function(){ | |
var firstHour= 9; | |
var lastHour = 17; | |
var days = 4; | |
for(var i = 0; i < courses.length; ++i){ | |
if(courses[i].earliest_time < firstHour){ | |
earliest = courses[i].earliest_time; | |
} | |
if(courses[i].latest_time > lastHour){ | |
lastHour = courses[i].latest_time; | |
} | |
if(courses[i].latest_day > days){ | |
days = courses[i].latest_day; | |
} | |
} | |
days++; | |
canvas.clear(); | |
width = $('#inpt-area').width(); | |
cols = days; | |
rows = lastHour-firstHour+1; | |
//Setting minimum size so still usable on mobile phones | |
if(canvas.pixelRatio > 1){ | |
var physicalWidth = width; | |
var physicalHeight = Math.ceil(physicalWidth*rows/(2*cols)); | |
width *= canvas.pixelRatio; | |
height = Math.ceil(width*rows/(2*cols)); | |
canvas.setHeight(height); | |
canvas.setWidth(width); | |
//Yes they are all required to avoid the canvas visually overflowing | |
$('#timetable').width(physicalWidth).height(physicalHeight); | |
$('.canvas-container').width(physicalWidth).height(physicalHeight); | |
$('.upper-canvas').width(physicalWidth).height(physicalHeight); | |
if(physicalWidth > 400){ | |
textScale = canvas.pixelRatio; | |
} else{ | |
textScale = 1.25; | |
} | |
} else if(width < 400){ | |
canvas.setZoom(width/400); | |
var act_width = width; | |
width = 400; | |
height = Math.ceil(width*rows/(2*cols)); | |
canvas.setHeight(Math.ceil(act_width*rows/(2*cols))); | |
canvas.setWidth(act_width); | |
} else{ | |
canvas.setZoom(1); | |
height = Math.ceil(width*rows/(2*cols)); | |
canvas.setHeight(height); | |
canvas.setWidth(width); | |
} | |
widthScale = Math.floor((width-yOffset)/cols); | |
heightScale = Math.ceil(widthScale/2); | |
var day = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', | |
'Saturday', 'Sunday']; | |
//Plot days | |
for(var i = 0; i < days; ++i){ | |
canvas.add(new fabric.Text(day[i],{ | |
fontSize: 12*textScale, | |
fontFamily: 'arial, sans-serif', | |
fontWeight: 'bold', | |
textAlign: 'center', | |
originX: 'center', | |
originY: 'top', | |
selectable: false, | |
left: i*widthScale + widthScale/2 + xOffset, | |
top: 0 | |
})); | |
} | |
//Plot times | |
for(var i = firstHour; i <= lastHour; ++i){ | |
canvas.add(new fabric.Text(i+':00',{ | |
fontSize: 12*textScale, | |
fontFamily: 'arial, sans-serif', | |
fontWeight: 'bold', | |
textAlign: 'center', | |
originX: 'center', | |
originY: 'top', | |
selectable: false, | |
left: 0, | |
top: (i-firstHour)*heightScale + heightScale/2 + yOffset, | |
angle: 270 | |
})); | |
} | |
//Plot gridlines | |
//Lines point array [x1,y1,x2,y2] | |
for (var i = 0; i < cols+1; ++i) { | |
var line = new fabric.Line([xOffset+i*widthScale, 0, | |
xOffset+i*widthScale, height], {stroke: '#ccc', selectable: false}); | |
canvas.add(line); | |
line.sendToBack(); | |
} | |
for (var i = 0; i < rows+1; ++i) { | |
var line = new fabric.Line([0, yOffset+i*heightScale, width, | |
yOffset+i*heightScale], {stroke: '#ccc', selectable: false}); | |
canvas.add(line); | |
line.sendToBack(); | |
} | |
}; | |
//Creates an individual class tile | |
var createClass = function(courseCode, classType, altPos, length, color, | |
dependants, thisPos){ | |
var text = new fabric.Text(courseCode+'\n'+classType, { | |
fontSize: 14*textScale, | |
fontFamily: 'arial, sans-serif', | |
fontWeight: 'bold', | |
textAlign: 'center', | |
originX: 'center', | |
originY: 'center', | |
selectable: false | |
}); | |
var container = new fabric.Rect({ | |
width: widthScale, | |
height: heightScale*length, | |
fill: color, | |
originX: 'center', | |
originY: 'center', | |
stroke: 'black', | |
strokeWidth: 1, | |
selectable: false, | |
opacity: 0.8 | |
}); | |
var subject = new fabric.Group([container, text], { | |
hasControls: false | |
}); | |
subject.courseCode = courseCode; | |
subject.classType = classType; | |
subject.thisPos = thisPos; | |
subject.classLen = length; | |
subject.altPos = altPos; | |
subject.dependants = dependants; | |
subject.currPos = [0, 0]; | |
subject.prevPos = subject.currPos; | |
canvas.add(subject); | |
return subject; | |
}; | |
//Adds all the classes contained in the courses array | |
//to the timetable | |
var addAllClasses = function(allowFull){ | |
var colorOptions = ['#6B64DE', '#4BBD59', '#FF635B', '#FFD95B', | |
'#8D48DB', '#33E8B2']; | |
var classObjs = []; | |
for(var i = 0; i < courses.length; ++i){ | |
classObjs = classObjs.concat( | |
addClasses(courses[i], colorOptions[i], allowFull)); | |
} | |
var classes = []; | |
var seen = []; | |
for(var i = classObjs.length-1; i >= 0; --i){ | |
if(seen.indexOf(classObjs[i].dependants[0]) < 0){ | |
classes.push(classObjs[i].dependants[0]); | |
seen.push(classObjs[i].dependants[0]); | |
} | |
} | |
depObjects = classes; | |
return classes; | |
}; | |
//Converts the date and time of subject to a coordinate for the | |
//grid displayed. | |
var dayTimeToGrid = function(day, time){ | |
return [day, time-startHour]; | |
}; | |
//Adds class blocks to canvas | |
var addClasses = function(course, color, allowFull){ | |
var uniClasses = Object.keys(course.class_types); | |
var addedClasses = []; | |
//Iterate over class types in this course | |
for(var i = 0; i < uniClasses.length; ++i){ | |
var altPos = []; //Array of alternate positions for class and dependants | |
var classGroup = course.class_types[uniClasses[i]]; | |
//Create class objects for the dependant classes | |
//Assumes that the first class represents the structure | |
//Of all class groups | |
var depClass = []; | |
for(var j = classGroup[0].length-1; j >= 0; --j){ | |
var len = classGroup[0][j].end_time - classGroup[0][j].start_time; | |
var position = depClass.length; | |
var toAdd = createClass(course.course_code, uniClasses[i], altPos, | |
len, color, depClass,position); | |
depClass.push(toAdd); | |
addedClasses.push(toAdd); | |
} | |
//Iterate over the possible class times | |
for(var j = classGroup.length-1; j >= 0; --j){ | |
depClass = []; | |
//Iterate over the dependant classes | |
for(var k = classGroup[j].length-1; k >= 0; --k){ | |
if(!allowFull && classGroup[j][k].status === 'Full') continue; | |
//grid position of class | |
var positionLength = dayTimeToGrid(classGroup[j][k].class_day, | |
classGroup[j][k].start_time); | |
//Push length of class | |
positionLength.push(classGroup[j][k].end_time - | |
classGroup[j][k].start_time); | |
depClass.push(positionLength); | |
} | |
//Exclude empty lines(usually from closed classes) | |
//Exclude duplicate entries (improves performance of generator) | |
if(depClass.length > 0){ | |
var valid = true; | |
for(var k = altPos.length-1; k >= 0; --k){ | |
for(var l = depClass.length-1; l >= 0; --l){ | |
if(depClass[l][0] === altPos[k][l][0] && | |
depClass[l][1] === altPos[k][l][1] && | |
depClass[l][2] === altPos[k][l][2]){ | |
valid = false; | |
break; | |
} | |
} | |
} | |
if(valid && depClass.length > 0){ | |
altPos.push(depClass); | |
} | |
} | |
} | |
//Randomize array | |
for (var j = altPos.length-1; j > 0; --j) { | |
var k = Math.floor(Math.random()*(j+1)); | |
var temp = altPos[j]; | |
altPos[j] = altPos[k]; | |
altPos[k] = temp; | |
} | |
} | |
return addedClasses; | |
}; | |
//Conditional snap to grid for objects | |
canvas.on('mouse:up', function(options){ | |
canvas.allowTouchScrolling = true; | |
var group = options.target; | |
for(var i = tempBlocks.length-1; i >= 0; --i){ | |
canvas.remove(tempBlocks[i]); | |
} | |
if(group && group.selectable){ | |
var dependants = group.dependants; | |
//Remove dashed line to from dependant blocks | |
for(var i = dependants.length-1; i >= 0; --i){ | |
dependants[i].getObjects()[0].strokeDashArray = null; | |
} | |
group.bringToFront(); | |
var x = Math.round(group.left/widthScale); | |
var y = Math.round(group.top/heightScale); | |
var index = -1; | |
var altPos = group.altPos; | |
var thisPos = group.thisPos; | |
//Snap to grid | |
for(var i = altPos.length-1; i >= 0; --i){ | |
if(altPos[i][thisPos][0] === x){ | |
if(altPos[i][thisPos][1] === y){ //Match in first square | |
index = i; | |
//Break means a first square match overrides any other | |
break; | |
} else if(y < altPos[i][thisPos][1] + group.classLen && | |
y > altPos[i][thisPos][1] - group.classLen){ | |
index = i; | |
} | |
} | |
} | |
//If block isnt snapped to grid, outline remains white | |
group.getObjects()[0].stroke = 'white'; | |
//Move any dependant classes | |
if(index >= 0){ | |
for(var i = dependants.length-1; i >= 0; --i){ | |
var x = altPos[index][i][0]; | |
var y = altPos[index][i][1]; | |
dependants[i].set({ | |
left: xOffset+widthScale*x, | |
top: yOffset+heightScale*y | |
}); | |
//Blocks snapped to grid have black outline | |
dependants[i].getObjects()[0].stroke = 'black'; | |
dependants[i].setCoords(); | |
dependants[i].prevPos = dependants[i].currPos; | |
dependants[i].currPos = [x, y]; | |
updateTableArray(dependants[i]); | |
} | |
} | |
canvas.renderAll(); | |
} | |
}); | |
//Highlighting possible positions for class to go | |
canvas.on('mouse:down', function(options){ | |
var group = canvas.findTarget(options.e); | |
if(group && group.selectable){ | |
canvas.allowTouchScrolling = false; | |
group.bringToFront(); | |
var altPos = group.altPos; | |
var thisPos = group.thisPos; | |
//Highlight alternative position blocks | |
//It is faster to make new blocks every time | |
//than to change the opacity | |
var color = group.getObjects()[0].fill; | |
var length = group.classLen; | |
for(var i = altPos.length-1; i >= 0; --i){ | |
var temp_block = new fabric.Rect({ | |
width: widthScale, | |
height: length*heightScale, | |
hasControls: false, | |
selectable: false, | |
fill: color, | |
originX: 'left', | |
originY: 'top', | |
stroke: 'black', | |
strokeWidth: 0.5, | |
opacity: 0.3, | |
left: yOffset+altPos[i][thisPos][0]*widthScale, | |
top: yOffset+altPos[i][thisPos][1]*heightScale | |
}); | |
tempBlocks.push(temp_block); | |
canvas.add(temp_block); | |
temp_block.bringToFront(); | |
} | |
//Add dashed line to all dependant blocks | |
for(var i = group.dependants.length-1; i >= 0; --i){ | |
group.dependants[i].getObjects()[0].strokeDashArray = [5,5]; | |
group.getObjects()[0].stroke = 'black' | |
} | |
group.setCoords(); | |
canvas.renderAll(); | |
} | |
}); | |
//Calculate offset whenever canvas is moved | |
canvas.on('after:render', function(){canvas.calcOffset();}); | |
//Generate a and load it to canvas | |
//Function will call a recursive backtracker to calculate a timetable | |
var generateTimetables = function(sortFuncs, clash){ | |
var classes = depObjects.concat(); | |
var timetable = []; | |
var solutions = []; | |
//Initialize array representation of timetable | |
for(var i = 0; i < cols; ++i){ | |
timetable.push([]); | |
for(var j = 0; j < rows; ++j){ | |
timetable[i].push([]); | |
} | |
} | |
//Check if any classes have only one option and place them if they do | |
//(improves performance when generating timetables) | |
for(var i = classes.length-1; i >= 0; --i){ | |
if(classes[i].altPos.length === 1){ | |
var toAdd = classes.splice(i, 1)[0]; | |
var altPos = toAdd.altPos; | |
var dependants = toAdd.dependants; | |
for(var j = dependants.length-1; j >= 0; --j){ | |
var x = altPos[0][j][0]; | |
var y = altPos[0][j][1]; | |
for(var k = dependants[j].classLen-1; k >= 0; --k){ | |
if(timetable[x][y+k].length > 0){ | |
--clash; | |
} | |
timetable[x][y+k].push([dependants[j], altPos[0][j][2]-k]); | |
} | |
} | |
} | |
} | |
//Recursive backtracking function | |
placeSubject(classes, timetable, solutions, clash); | |
if(solutions.length > 0){ | |
generatedTimetables = sortTimetables(solutions, sortFuncs); | |
drawTableArray(generatedTimetables[0]); | |
} | |
//If neither works, display error | |
else{ | |
canvas.clear(); | |
generatedTimetables = []; | |
canvas.add(new fabric.Text('Could not generate\nTry again', { | |
fontSize: 20*canvas.pixelRatio, | |
fontFamily: 'arial, sans-serif', | |
fontWeight: 'bold', | |
textAlign: 'center', | |
originX: 'center', | |
originY: 'top', | |
left: canvas.getWidth()/2, | |
top: 0, | |
selectable: false | |
})); | |
canvas.renderAll(); | |
$('#generate-fail').modal('show'); | |
} | |
}; | |
var sortTimetables = function(toBeSorted, sortFuncs){ | |
if(sortFuncs.length === 0) return; | |
//Decorate tables for first round of sorting | |
for(var i = toBeSorted.length-1; i >= 0; --i){ | |
toBeSorted[i].sortingData = [sortFuncs[0](toBeSorted[i])]; | |
} | |
//Sort first round solutions | |
toBeSorted.sort(function(a,b){ | |
if(a.sortingData[0] > b.sortingData[0]){ | |
return 1; | |
} else if(a.sortingData[0] < b.sortingData[0]){ | |
return -1; | |
} else{ | |
return 0; | |
} | |
}); | |
//Perform remaining rounds of sorting | |
for(var i = 1; i < sortFuncs.length; ++i){ | |
for(var j = toBeSorted.length-1; j >= 0; --j){ | |
toBeSorted[j].sortingData.push(sortFuncs[i](toBeSorted[j])); | |
} | |
} | |
toBeSorted.sort(function(a,b){ | |
for(var i = 0; i < sortFuncs.length; ++i){ | |
if(a.sortingData[i] > b.sortingData[i]){ | |
return 1; | |
} else if(a.sortingData[i] < b.sortingData[i]){ | |
return -1; | |
} | |
} | |
return 0; | |
}); | |
return toBeSorted; | |
}; | |
//Draws array timetable representation to canvas | |
var drawTableArray = function(table_arr){ | |
if(!table_arr){ | |
return; | |
} | |
for(var i = table_arr.length-1; i >= 0; --i){ | |
for(var j = table_arr[i].length-1; j >= 0; --j){ | |
var length = table_arr[i][j].length; | |
for(var k = length-1; k >= 0; --k){ | |
var group = table_arr[i][j][k][0]; | |
if(group.classLen === table_arr[i][j][k][1]){ | |
group.set({ | |
left: xOffset+widthScale*i, | |
top: yOffset+heightScale*j | |
}); | |
group.setCoords(); | |
group.currPos = [i, j]; | |
group.prevPos = group.currPos; | |
} | |
//Make outline red for clashes | |
if(length > 1){ | |
group.getObjects()[0].stroke = 'red'; | |
} | |
} | |
} | |
} | |
canvas.renderAll(); | |
}; | |
//Updates the array representation of the timetable when a class moves | |
//Also updates clash display outline for courses | |
var updateTableArray = function(classObj){ | |
var table = generatedTimetables[currDisplayed]; | |
var currPos = classObj.currPos; | |
var prevPos = classObj.prevPos; | |
var setRed = false; | |
for(var i = classObj.classLen-1; i >= 0; --i){ | |
var prevTime = table[prevPos[0]][prevPos[1]+i]; | |
var currTime = table[currPos[0]][currPos[1]+i]; | |
for(var j = prevTime.length-1; j >= 0; --j){ | |
if(prevTime[j][0] === classObj){ | |
var toMove = prevTime.splice(j, 1)[0]; | |
currTime.push(toMove); | |
} | |
if(!setRed && prevTime.length === 1){ | |
prevTime[0][0].getObjects()[0].stroke = 'black'; | |
} | |
if(currTime.length > 1){ | |
for(var k = currTime.length-1; k >= 0; --k){ | |
currTime[k][0].getObjects()[0].stroke = 'red'; | |
setRed = true; | |
} | |
} | |
} | |
} | |
}; | |
//Recursively place courses until a timetable is finished | |
//and fill array of solutions until limit is reached (or no more sol possible) | |
//Note: clashes not yet supported | |
var placeSubject = function(remaining_classes, curr_timetable, ret_arr, clash){ | |
//Cap number of solutions | |
if(ret_arr.length > 15000) return; | |
//Timetable is completed | |
if(remaining_classes.length === 0){ | |
ret_arr.push(curr_timetable); | |
return; | |
} | |
//Choose random remaining class so that there is a wider | |
//variety of solutions | |
var classes = remaining_classes.concat(); | |
var timetable = cloneTable(curr_timetable); | |
var uClass = classes.splice(Math.floor(Math.random()*classes.length), 1)[0]; | |
var altPos = uClass.altPos; | |
var dependants = uClass.dependants; | |
for(var i = altPos.length-1; i >= 0; --i){ //Down list | |
var maxClash = clash; | |
for(var j = altPos[i].length-1; j >= 0; --j){ //Across list | |
var x = altPos[i][j][0]; | |
var y = altPos[i][j][1]; | |
for(var k = dependants[j].classLen-1; k >= 0; --k){ | |
maxClash -= timetable[x][y+k].length; | |
} | |
} | |
if(maxClash >= 0){ | |
//Apply changes to timetable | |
for(var j = dependants.length-1; j >= 0; --j){ | |
var x = altPos[i][j][0]; | |
var y = altPos[i][j][1]; | |
for(var k = dependants[j].classLen-1; k >= 0; --k){ | |
timetable[x][y+k].push([dependants[j], altPos[i][j][2]-k]); | |
} | |
} | |
placeSubject(classes, timetable, ret_arr, maxClash); | |
timetable = cloneTable(curr_timetable); | |
} | |
} | |
}; | |
//Shallow clone multideminesional array | |
//Gets called frequently | |
var cloneTable = function(timetable){ | |
var new_table = []; | |
for(var i = 0; i < cols; ++i){ | |
new_table.push([]); | |
for(var j = 0; j < rows; ++j){ | |
//Concat is faster than slice | |
new_table[i].push(timetable[i][j].concat()); | |
} | |
} | |
return new_table; | |
}; | |
//Calculates the number of days at uni for given timetable | |
var table_calc_days = function(timetable){ | |
var days = 0; | |
for(var i = timetable.length-1; i >= 0; --i){ | |
for(var j = timetable[0].length-1; j >= 0; --j){ | |
if(timetable[i][j].length > 0){ | |
++days; | |
break; | |
} | |
} | |
} | |
return days; | |
}; | |
//Calculates the number of hours at uni for given timetable | |
var table_calc_hour = function(timetable){ | |
var hours = 0; | |
for(var i = timetable.length-1; i >= 0; --i){ | |
var start = 0; | |
var not_counting = true; | |
var finish = 0; | |
for(var j = timetable[0].length-1; j >= 0; --j){ | |
if(timetable[i][j].length !== 0){ | |
if(not_counting){ | |
start = j+1; | |
not_counting = false; | |
} else{ | |
finish = j; | |
} | |
} | |
} | |
hours += (start - finish); | |
} | |
return hours; | |
}; | |
//Calculates sleepin time | |
var table_calc_sleepin = function(timetable){ | |
var hours = 0; | |
for(var i = timetable.length-1; i >= 0; --i){ | |
var len = timetable[0].length; | |
for(var j = 0; j < len; ++j){ | |
if(timetable[i][j].length === 0){ | |
--hours; | |
} else{ | |
break; | |
} | |
} | |
} | |
return hours; | |
}; | |
//Calculate afternoon free time | |
var table_calc_afternoons = function(timetable){ | |
var hours = 0; | |
for(var i = timetable.length-1; i >= 0; --i){ | |
for(var j = timetable[0].length-1; j >= 0; --j){ | |
if(timetable[i][j].length === 0){ | |
--hours; | |
} else{ | |
break; | |
} | |
} | |
} | |
return hours; | |
}; | |
//Writes current data to cookie | |
var writeCookie = function(){ | |
document.cookie = 'bojanglesJSON='+ saveStateToJSON() + | |
'; expires=Fri, 31 Dec 2030 23:59:59 GMT'; | |
}; | |
//Reads subject data from cookie | |
var readCookie = function(){ | |
var cookieData = document.cookie.split('bojanglesJSON='); | |
if(cookieData){ | |
loadStateFromJSON(cookieData[1]); | |
} | |
}; | |
//Saves the timetable and options to a JSON representation | |
var saveStateToJSON = function(){ | |
if(generatedTimetables.length <= 0){ | |
return; | |
} | |
try{ | |
var jsonOut = { | |
courses: [], | |
clash: parseInt($('input:radio[name=clash-sel]:checked').val()), | |
allowFull: $('#incl-full').prop('checked'), | |
timetable: [] | |
} | |
for(var i = 0; i < courses.length; ++i){ | |
jsonOut.courses.push(courses[i].course_code); | |
} | |
for(var i = depObjects.length-1; i >= 0; --i){ | |
dependants = depObjects[i].dependants; | |
var classObj = { | |
courseCode: dependants[0].courseCode, | |
classType: dependants[0].classType, | |
classTimes: [] | |
} | |
jsonOut.timetable.push(classObj); | |
//Push subjects in same format as internal representation to json | |
//Start times are used in place of grid references for portability | |
for(var j = 0; j < dependants.length; ++j){ | |
classObj.classTimes.push([ | |
dependants[j].currPos[0], | |
dependants[j].currPos[1]+startHour, | |
dependants[j].classLen | |
]); | |
} | |
} | |
return JSON.stringify(jsonOut); | |
} | |
catch(err){ | |
throwWarning('Unable to save timetable, be sure to save as an image' + | |
'if you would like to keep it', true); | |
return ''; | |
} | |
}; | |
//Adds a warning with the given text to the page. Adds a severe warning if | |
//severe is truthy | |
var throwWarning = function(warning, severe){ | |
var html = '<div class="alert alert-'+ (severe ? 'danger' : 'warning') + | |
' alert-dismissible" role="alert"> <button type="button" class="close"' + | |
'data-dismiss="alert"><span aria-hidden="true">×</span><span class' + | |
'="sr-only">Close</span></button>' + warning + '</div>'; | |
$('#inpt-area').prepend(html); | |
}; | |
//Loads the timetable state from JSON | |
var loadStateFromJSON = function(jsonIn){ | |
jsonIn = JSON.parse(jsonIn); | |
console.log(jsonIn); | |
var jsonDeferred = []; | |
var addCourse = function(courseIn){ | |
courses.push(courseIn); | |
$('#chosen').append('<li class="list-group-item class-inpt">' + | |
courseIn.course_code + ' - ' + courseIn.course_name + | |
'<button class="btn btn-danger btn-xs pull-right rm-course">'+ | |
'Remove</btn></li>'); | |
if(jsonIn.warning){ | |
throwWarning(JsonIn.warning, false); | |
} | |
}; | |
for(var i = 0; i < jsonIn.courses.length; ++i){ | |
jsonDeferred.push($.getJSON('/data/bojangles/'+ jsonIn.courses[i] + | |
'.json', addCourse)); | |
} | |
//Wait until all ajax requests are served to start processing | |
$.when.apply($, jsonDeferred).done(function(){ | |
createTableGrid(); | |
var depObject = addAllClasses(jsonIn.allowFull); | |
console.log(depObject) | |
var inptTbl = jsonIn.timetable; | |
var timetable = []; | |
//Initialize array representation of timetable | |
for(var i = 0; i < cols; ++i){ | |
timetable.push([]); | |
for(var j = 0; j < rows; ++j){ | |
timetable[i].push([]); | |
} | |
} | |
//Each element in the saved timetable | |
for(var i = 0; i < inptTbl.length; ++i){ | |
//Each independant object for the given subject | |
for(var j = depObject.length-1; j >= 0; --j){ | |
if(depObject[j].courseCode === inptTbl[i].courseCode && | |
depObject[j].classType === inptTbl[i].classType){ | |
var altPos = depObject[j].altPos; | |
//Down the list of possible positions for the class | |
for(var k = altPos.length-1; k >= 0; --k){ | |
if(altPos[k].length != inptTbl[i].classTimes.length){ | |
continue; | |
} | |
var valid = true; | |
//Across the depedant classes list | |
for(var l = altPos[k].length-1; l >= 0; --l){ | |
if(altPos[k][l][0] != inptTbl[i].classTimes[l][0] || | |
altPos[k][l][1]+startHour != | |
inptTbl[i].classTimes[l][1] || | |
altPos[k][l][2] != inptTbl[i].classTimes[l][2]){ | |
valid = false; | |
break; | |
} | |
} | |
//Must match all fields to be valid | |
if(valid){ | |
var dependants = depObject[j].dependants; | |
//Across the dependant classes list | |
for(var l = dependants.length-1; l >= 0; --l){ | |
var x = altPos[k][l][0]; | |
var y = altPos[k][l][1]; | |
//Add object block in timetable to each hour | |
for(var m=dependants[l].classLen-1; m >= 0;--m){ | |
timetable[x][y+m].push([dependants[l], | |
altPos[k][l][2]-m]); | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
generatedTimetables.push(timetable); | |
drawTableArray(timetable); | |
}); | |
$('#incl-full').prop('checked', jsonIn.allowFull); | |
}; | |
$(document).ready(function(){ | |
createTableGrid(); | |
var clash = 0; | |
//Initialize tooltips | |
//Except on IOS devices where they do not work correctly | |
//due to problem in bootstrap. | |
//See: http://github.com/twbs/bootstrap/issues/6232 | |
if(!navigator.userAgent.match(/(iPad|iPhone|iPod)/g)){ | |
$('#typeaheadtooltip').tooltip(); | |
$('#sorting-opt').tooltip(); | |
$('#generate').tooltip(); | |
$('#reset').tooltip(); | |
$('#timetable').tooltip(); | |
$(document).on('mouseenter','[rel=tooltip]', function(){ | |
$(this).tooltip('show'); | |
}); | |
} | |
//Initialize buttons | |
$('.btn').each(function() { | |
$(this).button(); | |
}); | |
//Add subject to list when add is pressed | |
$('#course-srch').on('click', function() { | |
addSubject($('#course-inpt').val()); | |
$('#course-inpt').val(''); | |
}); | |
//Add subject to list when enter is pressed | |
$('#course-inpt').on('keypress', function(key){ | |
if(key.keyCode === 13){ | |
key.preventDefault(); | |
addSubject($(this).val()); | |
$(this).val(''); | |
} | |
}); | |
//Generate timetable when button is pressed | |
$('#generate').on('click', function(){ | |
clash = parseInt($('input:radio[name=clash-sel]:checked').val()); | |
createTableGrid(); | |
var sortOrderArr = sortOrder.toArray(); | |
var sortFuncs = []; | |
for(var i = 0; i < sortOrderArr.length; ++i){ | |
if(sortOrderArr[i] === 'days'){ | |
sortFuncs.push(table_calc_days); | |
} else if(sortOrderArr[i] === 'hours'){ | |
sortFuncs.push(table_calc_hour); | |
} else if(sortOrderArr[i] === 'sleepin'){ | |
sortFuncs.push(table_calc_sleepin); | |
} else if(sortOrderArr[i] === 'afternoon'){ | |
sortFuncs.push(table_calc_afternoons); | |
} else if(sortOrderArr[i] === 'random'){ | |
break; | |
} | |
} | |
$('#loading').fadeIn('fast'); | |
$('#bojangle-area').fadeTo('fast', 0.3, function(){ | |
addAllClasses($('#incl-full').prop('checked')); | |
generateTimetables(sortFuncs, clash); | |
$('#bojangle-area').fadeTo('fast', 1); | |
writeCookie(); | |
$('#loading').fadeOut('fast'); | |
}); | |
}); | |
//Save timetable as png | |
$('#save-btn').on('click', function(){ | |
canvas.deactivateAll().renderAll(); | |
var png = canvas.toDataURL('img/png'); | |
var newWindow = window.open(); | |
var html = '<html><head><title>timetable</title></head><body><img src='+ | |
png + '></body></html>' | |
$(newWindow.document.body).html(html); | |
}); | |
//Removing courses when button is pushed | |
$(document).on('click', '.rm-course', function(e){ | |
e.preventDefault(); | |
var $rm = $(this).parent(); | |
$rm.fadeOut(400, function(){ | |
var course_code = $rm.text().split(' - ')[0]; | |
for(var i = 0; i < courses.length; ++i){ | |
if(courses[i].course_code === course_code){ | |
courses.splice(i, 1); | |
} | |
} | |
$rm.remove(); | |
}); | |
}); | |
//Reset inputs and canvas when reset button pressed | |
$('#reset').on('click', function(){ | |
createTableGrid(9,17,5); | |
courses = []; | |
$('.class-inpt').each(function() { | |
$(this).fadeOut(400, function(){ | |
$(this).remove(); | |
}); | |
}); | |
writeCookie(); | |
}); | |
//Next timetable button | |
$('#next-tt').on('click', function(e){ | |
e.preventDefault(); | |
if(currDisplayed < generatedTimetables.length-1){ | |
++currDisplayed; | |
drawTableArray(generatedTimetables[currDisplayed]); | |
} | |
}); | |
//Previous timetable button | |
$('#prev-tt').on('click', function(e){ | |
e.preventDefault(); | |
if(currDisplayed > 0){ | |
--currDisplayed; | |
drawTableArray(generatedTimetables[currDisplayed]); | |
} | |
}); | |
//Check cookies | |
readCookie(); | |
}); | |
//Save cookie on page unload | |
$(window).on('unload', function() { | |
writeCookie(); | |
}); | |
//Resize canvas as the panel resizes | |
$(window).on('resize', function(){ | |
var new_width = $('#inpt-area').width(); | |
if(canvas.pixelRatio > 1){ | |
var new_height = Math.ceil(new_width*rows/(2*cols)); | |
$('#timetable').width(new_width).height(new_height); | |
$('.canvas-container').width(new_width).height(new_height); | |
$('.upper-canvas').width(new_width).height(new_height); | |
} else{ | |
canvas.setZoom(new_width/width); | |
canvas.setWidth(new_width); | |
canvas.setHeight(Math.ceil(new_width*rows/(2*cols))); | |
} | |
canvas.calcOffset(); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment