1 var express = require('express'); 2 var moment = require('moment') 3 var database = require('./database'); 4 var router = express.Router(); 5 var verify = require('./verify'); 6 7 8 /* GET users listing. */ 9 10 /** 11 * Middleware function to get matches for a user. 12 * API to access this function: GET /match/ 13 * @param {Object} req The express routing HTTP client request object. 14 * @param {Object} res The express routing HTTP client response object. 15 * @param {callback} next The express routing callback function to invoke next middleware in the stack. 16 * @return {Void} 17 */ 18 var matchGet = function(req, res, next) { 19 var db = req.app.locals.db; //get instance of db 20 var userId = parseInt(req.params.userid); 21 22 db.collection('Users') 23 .find({'userId': userId}) 24 .toArray(function(err, results) { 25 if (results.length == 0) { 26 res.status(404).send("404: userId not found"); 27 } else { 28 user = results[0]; // should only be one match 29 30 if (!verify.checkLogin(req.cookies.jwt, user.email)) { 31 res.status(401).redirect('/login'); 32 return; 33 } 34 35 var matchedResults = []; 36 matchUsers(req, res, next, userId, matchedResults); 37 38 //db call to insertEvent here: 39 database.insertEvent(database.routerProperties(req, res, next), matchedResults["user_id"], matchedResults["match_id"], matchedResults["event"], matchedResults["unix_time"], matchedResults["end_time"], matchedResults["location"]); 40 41 } 42 }); 43 } 44 45 router.get('/:userid', matchGet); 46 47 const MAX_AVAILABILITY_SCORE = 6; 48 const MAX_INTEREST_SCORE = 7.5; 49 const MAX_SKILL_SCORE = 5; 50 const NORMALIZED_BASE = 10.0; 51 const DAYS = 7; 52 const TIME_SLOTS = 48; 53 54 /** 55 * Key function to generate matches for a single user, given the user id. 56 * @param {Object} req The express routing HTTP client request object. 57 * @param {Object} res The express routing HTTP client response object. 58 * @param {callback} next The express routing callback function to invoke next middleware in the stack. 59 * @param {number} userId An integer that represents a user id. 60 * @return {!Array} A sorted array of matching users, from highest score match 61 * to lowest score match. 62 */ 63 function matchUsers(req, res, next, userId, matches) { 64 // Pass in objects of data for two users into match user function to get score match. 65 // Keep mapping of each user to score match (i.e. dictionary). Make sure that 66 // match is not attmepted for user with himself/herself. 67 let performMatch = function(curr_user, matchResults) { 68 currUser = curr_user[0]; 69 database.searchUsers(database.routerProperties(req, res, next), {"userId": {$ne: userId}}, matchResults, function(users, matchResultsArray) { 70 for (let i = 0; i < users.length; i ++) { 71 let potentialMatchUser = users[i]; 72 73 var info = matchUser(currUser, potentialMatchUser); 74 75 // info["event"] has the name, look up in database 76 77 // //searchActivities to get location for activity 78 // database.searchActivities(database.routerProperties(req, res, next), {"name": info["event"]}, function(activities) { 79 // console.log(activities[0]["locations"][0]); 80 // info["location"] = activities[0]["locations"][0]; 81 // }); 82 83 matchResultsArray.push(info); 84 // do something with result - or not, just keep appending to results and return 85 86 } 87 //console.log(JSON.stringify(matchResultsArray)); 88 //sorting matchResultsArray 89 matchResultsArray.sort(function(a, b){ 90 return b.score-a.score; 91 }); 92 93 //convert time to unix time 94 95 console.log(matchResultsArray); 96 //res.status(200).json(matchResultsArray); 97 }); 98 }; 99 database.searchUsers(database.routerProperties(req, res, next), {"userId": userId}, matches, performMatch); 100 } 101 102 /** 103 * Compute a match score for two users. 104 * @param {Object} curr_user An object containing the user profile information for the logged 105 * in user. 106 * @param {Object} potential_match An object containing the user profile information for the the 107 * match candidate for the logged in user. 108 * @return {Object} An object conaining matched activity, event time, and score. 109 * A score representing how good the match is between the logged in user and the potential match. 110 * This match is based on availability, interest level, and skill level. 111 */ 112 function matchUser(curr_user, potential_match) { 113 var availability_match_score = getAvailabilityMatchScore(curr_user["availability"], potential_match["availability"]); 114 var activity_match = getBestActivityMatch(curr_user["activities"], potential_match["activities"]); 115 var event_time = getEventStartTime(curr_user["availability"], potential_match["availability"]); 116 var total_score = 0; 117 var match = {}; 118 const MILLISECONDS_IN_HOUR = 3600000; 119 if (activity_match["interest_score"] != 0 && availability_match_score != 0) { 120 total_score = activity_match["interest_score"] + activity_match["skill_score"] + availability_match_score; 121 } 122 123 var unix_time = getEventDate(event_time[0], event_time[1]); 124 match["event"] = activity_match["name"]; 125 match["user_id"] = curr_user["userId"]; 126 match["score"] = total_score; 127 match["time"] = event_time; 128 match["unix_time"] = unix_time; //startTime in db 129 match["end_time"] = (unix_time + MILLISECONDS_IN_HOUR); //endTime in db 130 match["match_name"] = potential_match["name"]; 131 match["match_id"] = potential_match["userId"]; 132 match["match_email"] = potential_match["email"]; 133 match["location"] = getActivityLocations()[activity_match["name"]][0]; 134 135 return match; 136 } 137 138 /** 139 * Returns the best time to meet up for two users. 140 * This is done by computing the largest overlapping block of time between the two users, and 141 * finding the start time of this largest overlapping block. 142 * @param {Array<Array<<boolean>>} curr_user_availability A list of true/false availabilities 143 * for the current user for each thirty-minute time slot on each day of the week. 144 * @param {Array<Array<<boolean>>} potential_match_availability A list of true/false availabilities 145 * for the potentialMatch for each thirty-minute time slot on each day of the week. 146 * @return {Array<number>} A day and time slot for the start of the event. 147 */ 148 function getEventStartTime(curr_user_availability, potential_match_availability) { 149 var availability_match = getAvailabilityMatch(curr_user_availability, potential_match_availability); 150 var day = availability_match["day"]; 151 var start_time_slot = availability_match["slot"]; 152 return [day, start_time_slot]; 153 } 154 /** 155 * Returns a score for the availability match between two users. 156 * This is done by computing the largest overlapping block of time between the two users. 157 * This score is capped at a maximum defined by a constant MAX_AVAILABILITY_SCORE. 158 * @param {Array} curr_user_availability A list of true/false availabilities 159 * for the current user for each thirty-minute time slot on each day of the week. 160 * @param {Array} potential_match_availability A list of true/false availabilities 161 * for the potentialMatch for each thirty-minute time slot on each day of the week. 162 * @return {number} A score representing how good the time availability match is, where 0 represents 163 * a failed match and MAX_AVAILABILITY_SCORE is the highest possible match score for this category. 164 */ 165 function getAvailabilityMatchScore(curr_user_availability, potential_match_availability) { 166 var availability_match = getAvailabilityMatch(curr_user_availability, potential_match_availability); 167 var num_overlapping_periods = availability_match["periods"]; 168 169 // Thirty minutes is two short for an activity, so a match requires at least an hour of matched times. 170 // Furthermore, activity matches are not expected to have an activity for over three hours, 171 // so a maximum cutoff is set for highest possible match. 172 if (num_overlapping_periods <= 1) { 173 num_overlapping_periods = 0; 174 } else if (num_overlapping_periods > MAX_AVAILABILITY_SCORE) { 175 num_overlapping_periods = MAX_AVAILABILITY_SCORE; 176 } 177 return computeNormalizedScore(num_overlapping_periods, MAX_AVAILABILITY_SCORE); 178 } 179 180 181 /** 182 * Returns maximum number of consecutive overlapping half-hours for two users. This could 183 * span over multiple days of the week, as long as it is a consecutive chunk of hours (i.e. 184 * 11:00 PM on Monday until 1 AM on Tuesday). 185 * @param {Array} curr_user_availability A list of true/false availabilities 186 * for the current user for each thirty-minute time slot on each day of the week. 187 * @param {Array} potential_match_availability A list of true/false availabilities 188 * for the potentialMatch for each thirty-minute time slot on each day of the week. 189 * @return {Object} The day, start time slot, and maximum number of overlapping consecutive half-hours 190 * between the two users. 191 */ 192 function getAvailabilityMatch(curr_user_availability, potential_match_availability) { 193 var max_sequence = 0; 194 var curr_sequence = 0; 195 196 var curr_start_day = 0; 197 var curr_start_time_slot = 0; 198 var max_start_day = 0; 199 var max_start_time_slot = 0; 200 var already_started = false; 201 var availability_match = {}; 202 203 for (var i = 0; i < curr_user_availability.length; i++) { 204 for (var j = 0; j < curr_user_availability[i].length; j++) { 205 if (curr_user_availability[i][j] && potential_match_availability[i][j]) { 206 if (!already_started) { 207 curr_start_time_slot = j; 208 curr_start_day = i; 209 already_started = true; 210 } 211 curr_sequence++; 212 if (curr_sequence > max_sequence) { 213 max_start_day = curr_start_day; 214 max_start_time_slot = curr_start_time_slot; 215 max_sequence = curr_sequence; 216 } 217 } 218 else { 219 curr_sequence = 0; 220 already_started = false; 221 } 222 } 223 } 224 availability_match["day"] = max_start_day; 225 availability_match["slot"] = max_start_time_slot; 226 availability_match["periods"] = max_sequence; 227 return availability_match; 228 } 229 230 /** 231 * Returns an object containing the best activity match between two users and the 232 * interest and skill level scores for that activity. 233 * @param {Array} curr_user_activities A list of activities for the current user with 234 * interest and skill level scores. 235 * @param {Array} potential_match_activities A list of activities for the potential 236 * match user with interest and skill level scores. 237 * @return {Object} The matched activity yielding the highest score for the current user. 238 * The object contains a field for the activity name, a field for the interest match score, and 239 * a field for the skill match score. 240 */ 241 function getBestActivityMatch(curr_user_activities, potential_match_activities) { 242 return getBestActivityMatchFromList(generateActivityMatches(curr_user_activities, potential_match_activities)); 243 } 244 245 /** 246 * Generates a list of activity matches with an interest match and a skill match for each activity. 247 * @param {Array} curr_user_activities A list of activities for the current user with 248 * interest and skill level scores. 249 * @param {Array} potential_match_activities A list of activities for the potential 250 * match user with interest and skill level scores. 251 * @return {Array} List of objects with the potential match activities. Each object has 252 * the name of the potential match activity, a skill score, and an interest score. 253 */ 254 function generateActivityMatches(curr_user_activities, potential_match_activities) { 255 var activity_matches = []; 256 257 // Create a list with all activities with match score temporarily as 0. 258 for (var i = 0; i < curr_user_activities.length; i++) { 259 var activity_match = {}; 260 activity_match["name"] = curr_user_activities[i]["name"]; 261 activity_match["skill_score"] = 0; 262 activity_match["interest_score"] = 0; 263 activity_matches.push(activity_match); 264 } 265 266 // Fill array of matched activities. 267 for (var i = 0; i < curr_user_activities.length; i++) { 268 for (var j = 0; j < potential_match_activities.length; j++) { 269 if (curr_user_activities[i]["name"] !== potential_match_activities[j]["name"]) { 270 continue; 271 } 272 activity_matches[i]["skill_score"] = computeSkillMatch(curr_user_activities[i]["skill"], potential_match_activities[j]["skill"]); 273 activity_matches[i]["interest_score"] = computeInterestMatch(curr_user_activities[i]["interest"], potential_match_activities[j]["interest"]); 274 } 275 } 276 return activity_matches; 277 } 278 279 /** 280 * Returns an object containing the best activity match between two users and the 281 * interest and skill level scores for that activity, given a list of activities and scores. 282 * The best match is defined as having the highest skill score and interest score combined, 283 * disregarding activities with an interest match of 0. 284 * @param {Array} activity_matches Current activity matches with name, skill score, and interest 285 * score for each activity. 286 * @return {Object} The matched activity yielding the highest score for the current user. 287 * The object contains a field for the activity name, a field for the interest match score, and 288 * a field for the skill match score. 289 */ 290 function getBestActivityMatchFromList(activity_matches) { 291 var best_activity_match = {}; 292 // Default initialization so that keys are present in the object. When using the score in other functions, 293 // handle exceptional case of having 0 interest match score (meaning no match). 294 best_activity_match["name"] = "Lifting"; 295 best_activity_match["skill_score"] = 0; 296 best_activity_match["interest_score"] = 0; 297 var max_score = 0; 298 299 for (var i = 0; i < activity_matches.length; i++) { 300 if (activity_matches[i]["interest_score"] == 0) { 301 continue; 302 } else { 303 var curr_score = activity_matches[i]["interest_score"] + activity_matches[i]["skill_score"]; 304 if (curr_score > max_score) { 305 max_score = curr_score; 306 best_activity_match["name"] = activity_matches[i]["name"]; 307 best_activity_match["skill_score"] = activity_matches[i]["skill_score"]; 308 best_activity_match["interest_score"] = activity_matches[i]["interest_score"]; 309 } 310 } 311 } 312 return best_activity_match; 313 } 314 315 /** 316 * Returns a score for interest match. The computation most highly weights the current 317 * user's interests, and then weights the other user's interest level with secondary importance. 318 * Note that an interest level of 1 is the lowest possible interest for an activity, so this 319 * automatically results in a score of 0 for the interest match. 320 * @param {number} curr_user_interest Interest level for current user 321 * @param {number} potential_match_interest Interest level for other user 322 * @return {number} A normalized score representing the interest match between two users. 323 */ 324 function computeInterestMatch(curr_user_interest, potential_match_interest) { 325 var unnormalized_score = curr_user_interest + 0.5*(potential_match_interest); 326 if (curr_user_interest == 1 || potential_match_interest == 1) { 327 unnormalized_score = 0; 328 } 329 return computeNormalizedScore(unnormalized_score, MAX_INTEREST_SCORE); 330 } 331 332 /** 333 * Returns a score for skill match. The score is high if the two users have similar skill levels. 334 * @param {number} curr_user_skill Skill level of current user 335 * @param {number} potential_match_skill Skill level of potential match 336 * @return {number} A normalized score representing the skill level match between two users. 337 */ 338 function computeSkillMatch(curr_user_skill, potential_match_skill) { 339 var unnormalized_score = MAX_SKILL_SCORE - Math.abs(curr_user_skill-potential_match_skill) 340 return computeNormalizedScore(unnormalized_score, MAX_SKILL_SCORE); 341 } 342 343 /** 344 * Normalizes the score for the three categories, so all three types of scores 345 * have the same maximum and minimum values. 346 * @param {number} curr_score Current score for that category 347 * @param {number} curr_max Maximum score for that cateogry 348 * @return {number} A normalized score representing the match score between two users for one category. 349 */ 350 function computeNormalizedScore(curr_score, curr_max) { 351 return curr_score*1.0/curr_max*NORMALIZED_BASE; 352 } 353 354 /** 355 * Return the time of the event in Unix Epoch time format. 356 * @param {number} day Day in database for the event, where 0 is 357 * Monday and 6 is Sunday 358 * @param {number} time_slot The timeslot in the database for the 359 * event, where 0 means midnight, one means 12:30 AM, etc. 360 */ 361 function getEventDate(day, time_slot) { 362 // Convert days in database to match with day of the week in moment.js, 363 // which has Sunday as 0 and Saturday as 6. In the database, the index 364 // of 0 corresponds to Monday instead, which is why we need this line. 365 if (day === 6) { 366 day = 0; 367 } else { 368 day = day + 1; 369 } 370 371 var event_date = moment().day(day); 372 event_date = event_date.toDate(); 373 event_date.setHours(time_slot/2); 374 event_date.setMinutes((time_slot%2===0)?0:30); 375 event_date.setSeconds(0); 376 event_date.setMilliseconds(0); 377 return Date.parse(event_date); 378 } 379 380 /** 381 * Return the locations of activities. 382 * @return {Object} 383 */ 384 function getActivityLocations() { 385 var activity_locations = {"Lifting": ["Bfit", "Wooden"], "Running": ["Drake Stadium", "Perimeter run"], 386 "Swimming":["Sunset Rec", "SAC", "North Pool"], "Basketball": ["Hitch Courts", "Wooden"], 387 "Soccer": ["IM Field"], "Tennis": ["LA Tennis Courts"], "Volleyball": ["Sunset Rec"], 388 "Climbing": ["Wooden"], "Squash": ["Wooden"], "Frisbee": ["IM Field"]}; 389 390 return activity_locations; 391 } 392 393 module.exports = router; 394 395 //UNCOMMENT BELOW FOR MATCHING FUNCTION TESTING PURPOSES (and comment out above 'module.exports = router') 396 // module.exports = { 397 // MAX_AVAILABILITY_SCORE:MAX_AVAILABILITY_SCORE, 398 // MAX_INTEREST_SCORE:MAX_INTEREST_SCORE, 399 // MAX_SKILL_SCORE:MAX_SKILL_SCORE, 400 // NORMALIZED_BASE:NORMALIZED_BASE, 401 // DAYS:DAYS, 402 // TIME_SLOTS:TIME_SLOTS, 403 // matchUsers:matchUsers, 404 // matchUser:matchUser, 405 // getAvailabilityMatchScore:getAvailabilityMatchScore, 406 // getAvailabilityMatch:getAvailabilityMatch, 407 // getBestActivityMatch:getBestActivityMatch, 408 // router 409 // }; 410