Merge pull request #1343 from codecombat/master

Master into production (includes email system launch)
This commit is contained in:
Michael Schmatz 2014-07-17 08:57:04 -07:00
commit 2df9c96c45
13 changed files with 532 additions and 109 deletions

View file

@ -0,0 +1,17 @@
c = require './../schemas'
#This will represent transactional emails which have been sent
MailSentSchema = c.object {
title: 'Sent mail'
description: 'Emails which have been sent through the system'
}
_.extend MailSentSchema.properties,
mailTask: c.objectId {}
user: c.objectId links: [{rel: 'extra', href: '/db/user/{($)}'}]
sent: c.date title: 'Sent', readOnly: true
metadata: c.object {}, {}
c.extendBasicProperties MailSentSchema, 'mail.sent'
module.exports = MailSentSchema

View file

@ -26,5 +26,8 @@ block content
li(id="#{component.get('name')}#{doc.name}")
| #{doc.name}
ul.specialList
li!=marked(doc.description)
if doc.description[language.substring(1,language.length-1)]
li!=marked(doc.description[language.substring(1,language.length-1)])
else
li!=marked(doc.description)

View file

@ -19,10 +19,20 @@ module.exports = class UnnamedView extends RootView
onLoaded: ->
console.log 'we have the components...', (c.get('name') for c in @componentDocs.models)
console.log 'we have the attributes...', (c.attributes for c in @componentDocs.models)
if (me.get('aceConfig')?.language?) is false
console.log 'default language javascript'
else
console.log 'language is =', me.get('aceConfig').language
#console.log 'test', @componentDocs.models[99].attributes.propertyDocumentation[1].description['python']
super()
getRenderData: ->
c = super()
c.components = @componentDocs.models
c.marked = marked
if (me.get('aceConfig')?.language?) is false
c.language = 'javascript'
else
c.language = JSON.stringify(me.get('aceConfig').language)
c

View file

@ -59,7 +59,7 @@
"express-useragent": "~0.0.9",
"gridfs-stream": "0.4.x",
"stream-buffers": "0.2.x",
"sendwithus": "2.0.x",
"sendwithus": "2.1.x",
"aws-sdk": "~2.0.0",
"bayesian-battle": "0.0.x",
"redis": "",

View file

@ -0,0 +1,43 @@
config = require '../../server_config'
redis = require 'redis'
log = require 'winston'
class LockManager
constructor: ->
unless config.isProduction
throw "You shouldn't be instantiating distributed locks unless in production."
@redisNotAvailable = true
@redisClient = redis.createClient config.redis.port, config.redis.host
@redisClient.on "ready", =>
log.info "Redis ready!"
@redisNotAvailable = false
@redisClient.on "error", (err) =>
@redisNotAvailable = true
log.error "Redis connection error! Err: #{err}"
@redisClient.on "end", =>
@redisNotAvailable = true
log.error "Redis connection ended!"
@lockValues = {}
@unlockScript = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then return redis.call(\"del\",KEYS[1]) else return 0 end"
setLock: (lockName, timeoutMs, cb) =>
if @redisNotAvailable is true then return cb "Redis not available!"
randomNumber = Math.floor(Math.random() * 1000000000)
@redisClient.set [lockName,randomNumber, "NX", "PX", timeoutMs], (err, res) =>
if err? then return cb err, null
if res is "OK"
@lockValues[lockName] = randomNumber
return cb null, "Lock set!"
unless res
return cb "Lock already set!", null
releaseLock: (lockName, cb) =>
if @redisNotAvailable is true then return cb "Redis not available!"
@redisClient.eval [@unlockScript, 1, lockName, @lockValues[lockName]], (err, res) ->
if err? then return cb err, null
if res
cb null, "The lock was released!"
else
cb "The lock was not released.", null
module.exports = new LockManager()

View file

@ -9,6 +9,7 @@ module.exports.handlers =
'thang_type': 'levels/thangs/thang_type_handler'
'user': 'users/user_handler'
'user_remark': 'users/remarks/user_remark_handler'
'mail_sent': 'mail/sent/mail_sent_handler'
'achievement': 'achievements/achievement_handler'
'earned_achievement': 'achievements/earned_achievement_handler'

View file

@ -0,0 +1,11 @@
mongoose = require 'mongoose'
plugins = require '../../plugins/plugins'
jsonschema = require '../../../app/schemas/models/mail_sent'
MailSent = new mongoose.Schema({
sent:
type: Date
'default': Date.now
}, {strict: false})
module.exports = MailSent = mongoose.model('mail.sent', MailSent)

View file

@ -0,0 +1,12 @@
MailSent = require './MailSent'
Handler = require '../../commons/Handler'
class MailSentHandler extends Handler
modelClass: MailSent
editableProperties: ['mailTask','user','sent']
jsonSchema: require '../../../app/schemas/models/mail_sent'
hasAccess: (req) ->
req.user?.isAdmin()
module.exports = new MailSentHandler()

View file

@ -117,7 +117,8 @@ module.exports.setup = (app) ->
)
)
app.get '/auth/unsubscribe', (req, res) ->
app.get '/auth/unsubscribe', (req, res) ->
req.query.email = decodeURIComponent(req.query.email)
email = req.query.email
unless req.query.email
return errors.badInput res, 'No email provided to unsubscribe.'
@ -131,7 +132,7 @@ module.exports.setup = (app) ->
return errors.serverError res, 'Database failure.' if err
res.send "Unsubscribed #{req.query.email} from CodeCombat emails for #{session.levelName} #{session.team} ladder updates. Sorry to see you go! <p><a href='/play/ladder/#{session.levelID}#my-matches'>Ladder preferences</a></p>"
res.end()
User.findOne({emailLower: req.query.email.toLowerCase()}).exec (err, user) ->
if not user
return errors.notFound res, "No user found with email '#{req.query.email}'"
@ -143,7 +144,11 @@ module.exports.setup = (app) ->
emails.recruitNotes ?= {}
emails.recruitNotes.enabled = false
msg = "Unsubscribed #{req.query.email} from recruiting emails."
else if req.query.employerNotes
emails.employerNotes ?= {}
emails.employerNotes.enabled = false
msg = "Unsubscribed #{req.query.email} from employer emails."
else
msg = "Unsubscribed #{req.query.email} from all CodeCombat emails. Sorry to see you go!"
emailSettings.enabled = false for emailSettings in _.values(emails)

View file

@ -1,17 +1,330 @@
mail = require '../commons/mail'
MailSent = require '../mail/sent/MailSent'
User = require '../users/User'
async = require 'async'
errors = require '../commons/errors'
config = require '../../server_config'
LevelSession = require '../levels/sessions/LevelSession'
Level = require '../levels/Level'
log = require 'winston'
sendwithus = require '../sendwithus'
if config.isProduction
lockManager = require '../commons/LockManager'
module.exports.setup = (app) ->
app.all config.mail.mailchimpWebhook, handleMailchimpWebHook
app.get '/mail/cron/ladder-update', handleLadderUpdate
if lockManager
setupScheduledEmails()
setupScheduledEmails = ->
testForLockManager()
mailTasks = [
taskFunction: candidateUpdateProfileTask
frequencyMs: 10 * 60 * 1000 #10 minutes
,
taskFunction: internalCandidateUpdateTask
frequencyMs: 10 * 60 * 1000 #10 minutes
,
taskFunction: employerNewCandidatesAvailableTask
frequencyMs: 10 * 60 * 1000 #10 minutes
]
for mailTask in mailTasks
setInterval mailTask.taskFunction, mailTask.frequencyMs
testForLockManager = -> unless lockManager then throw "The system isn't configured to do distributed locking!"
### Candidate Update Reminder Task ###
candidateUpdateProfileTask = ->
mailTaskName = "candidateUpdateProfileTask"
lockDurationMs = 2 * 60 * 1000
currentDate = new Date()
timeRanges = []
for weekPair in [[4, 2,'two weeks'], [8, 4, 'four weeks'], [8, 52, 'eight weeks']]
timeRanges.push
start: generateWeekOffset currentDate, weekPair[0]
end: generateWeekOffset currentDate, weekPair[1]
name: weekPair[2]
lockManager.setLock mailTaskName, lockDurationMs, (err) ->
if err? then return log.error "Error getting a distributed lock for task #{mailTaskName}: #{err}"
async.each timeRanges, emailTimeRange.bind({mailTaskName: mailTaskName}), (err) ->
if err
log.error "There was an error sending the candidate profile update reminder emails: #{err}"
else
log.info "Completed mail task #{mailTaskName}"
lockManager.releaseLock mailTaskName, (err) ->
if err? then return log.error "There was an error releasing the distributed lock for task #{mailTaskName}: #{err}"
generateWeekOffset = (originalDate, numberOfWeeks) ->
return (new Date(originalDate.getTime() - numberOfWeeks * 7 * 24 * 60 * 60 * 1000)).toISOString()
emailTimeRange = (timeRange, emailTimeRangeCallback) ->
waterfallContext =
"timeRange": timeRange
"mailTaskName": @mailTaskName
async.waterfall [
findAllCandidatesWithinTimeRange.bind(waterfallContext)
(unfilteredCandidates, cb) ->
async.reject unfilteredCandidates, candidateFilter.bind(waterfallContext), cb.bind(null, null)
(filteredCandidates, cb) ->
async.each filteredCandidates, sendReminderEmailToCandidate.bind(waterfallContext), cb
], emailTimeRangeCallback
findAllCandidatesWithinTimeRange = (cb) ->
findParameters =
"jobProfile.updated":
$gt: @timeRange.start
$lte: @timeRange.end
"jobProfileApproved": true
selection = "_id email jobProfile.name jobProfile.updated emails" #make sure to check for anyNotes too.
User.find(findParameters).select(selection).lean().exec cb
candidateFilter = (candidate, sentEmailFilterCallback) ->
if candidate.emails?.anyNotes?.enabled is false or candidate.emails?.recruitNotes?.enabled is false
return sentEmailFilterCallback true
findParameters =
"user": candidate._id
"mailTask": @mailTaskName
"metadata.timeRangeName": @timeRange.name
"metadata.updated": candidate.jobProfile.updated
MailSent.find(findParameters).lean().exec (err, sentMail) ->
if err?
log.error "Error finding mail sent for task #{@mailTaskName} and user #{candidate._id}!"
sentEmailFilterCallback true
else
sentEmailFilterCallback Boolean(sentMail.length)
findEmployersSignedUpAfterDate = (dateObject, cb) ->
countParameters =
$or: [{"dateCreated": {$gte: dateObject}},{"signedEmployerAgreement":{$gte: dateObject}}]
employerAt: {$exists: true}
permissions: "employer"
User.count countParameters, cb
sendReminderEmailToCandidate = (candidate, sendEmailCallback) ->
findEmployersSignedUpAfterDate new Date(candidate.jobProfile.updated), (err, employersAfterCount) =>
if err?
log.error "There was an error finding employers who signed up after #{candidate.jobProfile.updated}: #{err}"
return sendEmailCallback err
if employersAfterCount < 2
employersAfterCount = 2
context =
email_id: "tem_CtTLsKQufxrxoPMn7upKiL"
recipient:
address: candidate.email
name: candidate.jobProfile.name
email_data:
new_company: employersAfterCount
company_name: "CodeCombat"
user_profile: "http://codecombat.com/account/profile/#{candidate._id}"
recipient_address: encodeURIComponent(candidate.email)
log.info "Sending #{@timeRange.name} update reminder to #{context.recipient.name}(#{context.recipient.address})"
newSentMail =
mailTask: @mailTaskName
user: candidate._id
metadata:
timeRangeName: @timeRange.name
updated: candidate.jobProfile.updated
MailSent.create newSentMail, (err) ->
if err? then return sendEmailCallback err
sendwithus.api.send context, (err, result) ->
log.error "Error sending candidate update reminder email: #{err} with result #{result}" if err
sendEmailCallback null
### End Candidate Update Reminder Task ###
### Internal Candidate Update Reminder Email ###
internalCandidateUpdateTask = ->
mailTaskName = "internalCandidateUpdateTask"
lockDurationMs = 2 * 60 * 1000
lockManager.setLock mailTaskName, lockDurationMs, (err) ->
if err? then return log.error "Error getting a distributed lock for task #{mailTaskName}: #{err}"
emailInternalCandidateUpdateReminder.call {"mailTaskName":mailTaskName}, (err) ->
if err
log.error "There was an error sending the internal candidate update reminder.: #{err}"
else
log.info "Sent internal candidate update reminder email!"
lockManager.releaseLock mailTaskName, (err) ->
if err? then return log.error "There was an error releasing the distributed lock for task #{mailTaskName}: #{err}"
emailInternalCandidateUpdateReminder = (internalCandidateUpdateReminderCallback) ->
currentTime = new Date()
beginningOfUTCDay = new Date()
beginningOfUTCDay.setUTCHours(0,0,0,0)
asyncContext =
"beginningOfUTCDay": beginningOfUTCDay
"currentTime": currentTime
"mailTaskName": @mailTaskName
async.waterfall [
findNonApprovedCandidatesWhoUpdatedJobProfileToday.bind(asyncContext)
(unfilteredCandidates, cb) ->
async.reject unfilteredCandidates, candidatesUpdatedTodayFilter.bind(asyncContext), cb.bind(null,null)
(filteredCandidates, cb) ->
async.each filteredCandidates, sendInternalCandidateUpdateReminder.bind(asyncContext), cb
], internalCandidateUpdateReminderCallback
findNonApprovedCandidatesWhoUpdatedJobProfileToday = (cb) ->
findParameters =
"jobProfile.updated":
$lte: @currentTime.toISOString()
$gt: @beginningOfUTCDay.toISOString()
"jobProfileApproved": false
User.find(findParameters).select("_id jobProfile.name jobProfile.updated").lean().exec cb
candidatesUpdatedTodayFilter = (candidate, cb) ->
findParameters =
"user": candidate._id
"mailTask": @mailTaskName
"metadata.beginningOfUTCDay": @beginningOfUTCDay
MailSent.find(findParameters).lean().exec (err, sentMail) ->
if err?
log.error "Error finding mail sent for task #{@mailTaskName} and user #{candidate._id}!"
cb true
else
cb Boolean(sentMail.length)
sendInternalCandidateUpdateReminder = (candidate, cb) ->
context =
email_id: "tem_Ac7nhgKqatTHBCgDgjF5pE"
recipient:
address: "team@codecombat.com"
name: "The CodeCombat Team"
email_data:
new_candidate_profile: "https://codecombat.com/account/profile/#{candidate._id}"
log.info "Sending candidate updated reminder for #{candidate.jobProfile.name}"
newSentMail =
mailTask: @mailTaskName
user: candidate._id
metadata:
beginningOfUTCDay: @beginningOfUTCDay
MailSent.create newSentMail, (err) ->
if err? then return cb err
sendwithus.api.send context, (err, result) ->
log.error "Error sending interal candidate update email: #{err} with result #{result}" if err
cb null
### End Internal Candidate Update Reminder Email ###
### Employer New Candidates Available Email ###
employerNewCandidatesAvailableTask = ->
mailTaskName = "employerNewCandidatesAvailableTask"
lockDurationMs = 2 * 60 * 1000
lockManager.setLock mailTaskName, lockDurationMs, (err) ->
if err? then return log.error "Error getting a distributed lock for task #{mailTaskName}: #{err}"
emailEmployerNewCandidatesAvailable.call {"mailTaskName":mailTaskName}, (err) ->
if err
log.error "There was an error completing the new candidates available task: #{err}"
else
log.info "Completed the employer new candidates available task!"
lockManager.releaseLock mailTaskName, (err) ->
if err? then return log.error "There was an error releasing the distributed lock for task #{mailTaskName}: #{err}"
emailEmployerNewCandidatesAvailable = (emailEmployerNewCandidatesAvailableCallback) ->
currentTime = new Date()
asyncContext =
"currentTime": currentTime
"mailTaskName": @mailTaskName
async.waterfall [
findAllEmployers
makeEmployerNamesEasilyAccessible
(allEmployers, cb) ->
async.reject allEmployers, employersEmailedDigestMoreThanWeekAgoFilter.bind(asyncContext), cb.bind(null,null)
(employersToEmail, cb) ->
async.each employersToEmail, sendEmployerNewCandidatesAvailableEmail.bind(asyncContext), cb
], emailEmployerNewCandidatesAvailableCallback
findAllEmployers = (cb) ->
findParameters =
"employerAt":
$exists: true
permissions: "employer"
selection = "_id email employerAt signedEmployerAgreement.data.firstName signedEmployerAgreement.data.lastName activity dateCreated emails"
User.find(findParameters).select(selection).lean().exec cb
makeEmployerNamesEasilyAccessible = (allEmployers, cb) ->
for employer, index in allEmployers
if employer.signedEmployerAgreement?.data?.firstName
employer.name = employer.signedEmployerAgreement.data.firstName + " " + employer.signedEmployerAgreement.data.lastName
delete employer.signedEmployerAgreement
allEmployers[index] = employer
cb null, allEmployers
employersEmailedDigestMoreThanWeekAgoFilter = (employer, cb) ->
if employer.emails?.employerNotes?.enabled is false
return sentEmailFilterCallback true
findParameters =
"user": employer._id
"mailTask": @mailTaskName
"sent":
$gt: new Date(@currentTime.getTime() - 7 * 24 * 60 * 60 * 1000)
MailSent.find(findParameters).lean().exec (err, sentMail) ->
if err?
log.error "Error finding mail sent for task #{@mailTaskName} and employer #employer._id}!"
cb true
else
cb Boolean(sentMail.length)
sendEmployerNewCandidatesAvailableEmail = (employer, cb) ->
lastLoginDate = employer.activity?.login?.last ? employer.dateCreated
countParameters =
"jobProfileApproved": true
$or: [
jobProfileApprovedDate:
$gt: lastLoginDate.toISOString()
,
jobProfileApprovedDate:
$exists: false
"jobProfile.updated":
$gt: lastLoginDate.toISOString()
]
User.count countParameters, (err, numberOfCandidatesSinceLogin) =>
if err? then return cb err
context =
email_id: "tem_CCcHKr95Nvu5bT7c7iHCtm"
recipient:
address: employer.email
email_data:
new_candidates: numberOfCandidatesSinceLogin
employer_company_name: employer.employerAt
company_name: "CodeCombat"
recipient_address: encodeURIComponent(employer.email)
if employer.name
context.recipient.name = employer.name
log.info "Sending available candidates update reminder to #{context.recipient.name}(#{context.recipient.address})"
newSentMail =
mailTask: @mailTaskName
user: employer._id
MailSent.create newSentMail, (err) ->
if err? then return cb err
sendwithus.api.send context, (err, result) ->
log.error "Error sending employer candidates available email: #{err} with result #{result}" if err
cb null
### End Employer New Candidates Available Email ###
### New Recruit Leaderboard Email ###
###
newRecruitLeaderboardEmailTask = ->
# tem_kMQFCKX3v4DNAQDsMAsPJC
#maxRank and maxRankTime should be recorded if isSimulating is false
mailTaskName = "newRecruitLeaderboardEmailTask"
lockDurationMs = 6000
lockManager.setLock mailTaskName, lockDurationMs, (err, lockResult) ->
###
### End New Recruit Leaderboard Email ###
### Employer Matching Candidate Notification Email ###
###
employerMatchingCandidateNotificationTask = ->
# tem_mYsepTfWQ265noKfZJcbBH
#save email filters in their own collection
mailTaskName = "employerMatchingCandidateNotificationTask"
lockDurationMs = 6000
lockManager.setLock mailTaskName, lockDurationMs, (err, lockResult) ->
###
### End Employer Matching Candidate Notification Email ###
### Ladder Update Email ###
DEBUGGING = false
LADDER_PREGAME_INTERVAL = 2 * 3600 * 1000 # Send emails two hours before players last submitted.
@ -28,6 +341,8 @@ isRequestFromDesignatedCronHandler = (req, res) ->
return false
return true
handleLadderUpdate = (req, res) ->
log.info('Going to see about sending ladder update emails.')
requestIsFromDesignatedCronHandler = isRequestFromDesignatedCronHandler req, res
@ -151,6 +466,7 @@ getScoreHistoryGraphURL = (session, daysAgo) ->
chartData = times.join(',') + '|' + scores.join(',')
"https://chart.googleapis.com/chart?chs=600x75&cht=lxy&chtt=Score%3A+#{currentScore}&chts=222222,12,r&chf=a,s,000000FF&chls=2&chd=t:#{chartData}&chxt=y&chxr=0,#{minScore},#{maxScore}"
### End Ladder Update Email ###
handleMailchimpWebHook = (req, res) ->
post = req.body

View file

@ -12,6 +12,10 @@ config.mongo =
host: process.env.COCO_MONGO_HOST or 'localhost'
db: process.env.COCO_MONGO_DATABASE_NAME or 'coco'
mongoose_replica_string: process.env.COCO_MONGO_MONGOOSE_REPLICA_STRING or ''
config.redis =
port: process.env.COCO_REDIS_PORT or 6379
host: process.env.COCO_REDIS_HOST or 'localhost'
if config.unittest
config.port += 1

View file

@ -3,97 +3,98 @@ describe 'Ellipse', ->
Rectangle = require 'lib/world/rectangle'
Vector = require 'lib/world/vector'
#it 'contains its own center', ->
# ellipse = new Ellipse 0, 0, 10, 10
# expect(ellipse.containsPoint(new Vector 0, 0)).toBe true
#
#it 'contains a point when rotated', ->
# ellipse = new Ellipse 0, -20, 40, 40, 3 * Math.PI / 4
# p = new Vector 0, 2
# expect(ellipse.containsPoint(p, true)).toBe true
#
#it 'contains more points properly', ->
# # ellipse with y major axis, off-origin center, and 45 degree rotation
# ellipse = new Ellipse 1, 2, 4, 6, Math.PI / 4
# expect(ellipse.contains new Vector(1, 2)).toBe true
# expect(ellipse.contains new Vector(-1, 3)).toBe true
# expect(ellipse.contains new Vector(0, 4)).toBe true
# expect(ellipse.contains new Vector(1, 4)).toBe true
# expect(ellipse.contains new Vector(3, 0)).toBe true
# expect(ellipse.contains new Vector(1, 0)).toBe true
# expect(ellipse.contains new Vector(0, 1)).toBe true
# expect(ellipse.contains new Vector(-1, 2)).toBe true
# expect(ellipse.contains new Vector(2, 2)).toBe true
# expect(ellipse.contains new Vector(0, 0)).toBe false
# expect(ellipse.contains new Vector(0, 5)).toBe false
# expect(ellipse.contains new Vector(3, 4)).toBe false
# expect(ellipse.contains new Vector(4, 0)).toBe false
# expect(ellipse.contains new Vector(2, -1)).toBe false
# expect(ellipse.contains new Vector(0, -3)).toBe false
# expect(ellipse.contains new Vector(-2, -2)).toBe false
# expect(ellipse.contains new Vector(-2, 0)).toBe false
# expect(ellipse.contains new Vector(-2, 4)).toBe false
#
#it 'correctly calculates distance to a faraway point', ->
# ellipse = new Ellipse 100, 50, 20, 40
# p = new Vector 200, 300
# d = 10 * Math.sqrt(610)
# expect(ellipse.distanceToPoint(p)).toBeCloseTo d
# ellipse.rotation = Math.PI / 2
# d = 80 * Math.sqrt(10)
# expect(ellipse.distanceToPoint(p)).toBeCloseTo d
#
#it 'does not modify itself or target Vector when calculating distance', ->
# ellipse = new Ellipse -100, -200, 1, 100
# ellipse2 = ellipse.copy()
# p = new Vector -100.25, -101
# p2 = p.copy()
# ellipse.distanceToPoint(p)
# expect(p.x).toEqual p2.x
# expect(p.y).toEqual p2.y
# expect(ellipse.x).toEqual ellipse2.x
# expect(ellipse.y).toEqual ellipse2.y
# expect(ellipse.width).toEqual ellipse2.width
# expect(ellipse.height).toEqual ellipse2.height
# expect(ellipse.rotation).toEqual ellipse2.rotation
#
#it 'correctly calculates distance to contained point', ->
# ellipse = new Ellipse -100, -200, 1, 100
# ellipse2 = ellipse.copy()
# p = new Vector -100.25, -160
# p2 = p.copy()
# expect(ellipse.distanceToPoint(p)).toBe 0
# ellipse.rotation = 0.00000001 * Math.PI
# expect(ellipse.distanceToPoint(p)).toBe 0
#
#it 'AABB works when not rotated', ->
# ellipse = new Ellipse 10, 20, 30, 40
# rect = new Rectangle 10, 20, 30, 40
# aabb1 = ellipse.axisAlignedBoundingBox()
# aabb2 = ellipse.axisAlignedBoundingBox()
# for prop in ['x', 'y', 'width', 'height']
# expect(aabb1[prop]).toBe aabb2[prop]
#
#it 'AABB works when rotated', ->
# ellipse = new Ellipse 10, 20, 30, 40, Math.PI / 3
# rect = new Rectangle 10, 20, 30, 40, Math.PI / 3
# aabb1 = ellipse.axisAlignedBoundingBox()
# aabb2 = ellipse.axisAlignedBoundingBox()
# for prop in ['x', 'y', 'width', 'height']
# expect(aabb1[prop]).toBe aabb2[prop]
#
#it 'calculates ellipse intersections properly', ->
# # ellipse with y major axis, off-origin center, and 45 degree rotation
# ellipse = new Ellipse 1, 2, 4, 6, Math.PI / 4
# expect(ellipse.intersectsShape new Rectangle(0, 0, 2, 2, 0)).toBe true
# expect(ellipse.intersectsShape new Rectangle(0, -1, 2, 3, 0)).toBe true
# expect(ellipse.intersectsShape new Rectangle(-1, -0.5, 2 * Math.SQRT2, 2 * Math.SQRT2, Math.PI / 4)).toBe true
# expect(ellipse.intersectsShape new Rectangle(-1, -0.5, 2 * Math.SQRT2, 2 * Math.SQRT2, 0)).toBe true
# expect(ellipse.intersectsShape new Rectangle(-1, -1, 2 * Math.SQRT2, 2 * Math.SQRT2, 0)).toBe true
# expect(ellipse.intersectsShape new Rectangle(-1, -1, 2 * Math.SQRT2, 2 * Math.SQRT2, Math.PI / 4)).toBe false
# expect(ellipse.intersectsShape new Rectangle(-2, -2, 2, 2, 0)).toBe false
# expect(ellipse.intersectsShape new Rectangle(-Math.SQRT2 / 2, -Math.SQRT2 / 2, Math.SQRT2, Math.SQRT2, 0)).toBe false
# expect(ellipse.intersectsShape new Rectangle(-Math.SQRT2 / 2, -Math.SQRT2 / 2, Math.SQRT2, Math.SQRT2, Math.PI / 4)).toBe false
# expect(ellipse.intersectsShape new Rectangle(-2, 0, 2, 2, 0)).toBe false
# expect(ellipse.intersectsShape new Rectangle(0, -2, 2, 2, 0)).toBe false
# expect(ellipse.intersectsShape new Rectangle(1, 2, 1, 1, 0)).toBe true
it 'contains its own center', ->
ellipse = new Ellipse 0, 0, 10, 10
expect(ellipse.containsPoint(new Vector 0, 0)).toBe true
it 'contains a point when rotated', ->
ellipse = new Ellipse 0, -20, 40, 40, 3 * Math.PI / 4
expect(ellipse.containsPoint new Vector(0, 0)).toBe true
expect(ellipse.containsPoint new Vector(0, 2)).toBe false
it 'contains more points properly', ->
# ellipse with y major axis, off-origin center, and 45 degree rotation
ellipse = new Ellipse 1, 2, 4, 6, Math.PI / 4
expect(ellipse.containsPoint new Vector(1, 2)).toBe true
expect(ellipse.containsPoint new Vector(-1, 3)).toBe true
expect(ellipse.containsPoint new Vector(0, 4)).toBe true
expect(ellipse.containsPoint new Vector(1, 4)).toBe true
expect(ellipse.containsPoint new Vector(3, 0)).toBe true
expect(ellipse.containsPoint new Vector(1, 0)).toBe true
expect(ellipse.containsPoint new Vector(0, 1)).toBe true
expect(ellipse.containsPoint new Vector(-1, 2)).toBe true
expect(ellipse.containsPoint new Vector(2, 2)).toBe true
expect(ellipse.containsPoint new Vector(0, 0)).toBe false
expect(ellipse.containsPoint new Vector(0, 5)).toBe false
expect(ellipse.containsPoint new Vector(3, 4)).toBe false
expect(ellipse.containsPoint new Vector(4, 0)).toBe false
expect(ellipse.containsPoint new Vector(2, -1)).toBe false
expect(ellipse.containsPoint new Vector(0, -3)).toBe false
expect(ellipse.containsPoint new Vector(-2, -2)).toBe false
expect(ellipse.containsPoint new Vector(-2, 0)).toBe false
expect(ellipse.containsPoint new Vector(-2, 4)).toBe false
xit 'correctly calculates distance to a faraway point', ->
# TODO: this is the correct distance if the ellipse were a rectangle, but need to update for actual ellipse expected distances.
ellipse = new Ellipse 100, 50, 20, 40
p = new Vector 200, 300
d = 10 * Math.sqrt(610)
expect(ellipse.distanceToPoint(p)).toBeCloseTo d
ellipse.rotation = Math.PI / 2
d = 80 * Math.sqrt(10)
expect(ellipse.distanceToPoint(p)).toBeCloseTo d
it 'does not modify itself or target Vector when calculating distance', ->
ellipse = new Ellipse -100, -200, 1, 100
ellipse2 = ellipse.copy()
p = new Vector -100.25, -101
p2 = p.copy()
ellipse.distanceToPoint(p)
expect(p.x).toEqual p2.x
expect(p.y).toEqual p2.y
expect(ellipse.x).toEqual ellipse2.x
expect(ellipse.y).toEqual ellipse2.y
expect(ellipse.width).toEqual ellipse2.width
expect(ellipse.height).toEqual ellipse2.height
expect(ellipse.rotation).toEqual ellipse2.rotation
it 'correctly calculates distance to contained point', ->
ellipse = new Ellipse -100, -200, 1, 100
ellipse2 = ellipse.copy()
p = new Vector -100.25, -160
p2 = p.copy()
expect(ellipse.distanceToPoint(p)).toBe 0
ellipse.rotation = 0.00000001 * Math.PI
expect(ellipse.distanceToPoint(p)).toBe 0
it 'AABB works when not rotated', ->
ellipse = new Ellipse 10, 20, 30, 40
rect = new Rectangle 10, 20, 30, 40
aabb1 = ellipse.axisAlignedBoundingBox()
aabb2 = ellipse.axisAlignedBoundingBox()
for prop in ['x', 'y', 'width', 'height']
expect(aabb1[prop]).toBe aabb2[prop]
it 'AABB works when rotated', ->
ellipse = new Ellipse 10, 20, 30, 40, Math.PI / 3
rect = new Rectangle 10, 20, 30, 40, Math.PI / 3
aabb1 = ellipse.axisAlignedBoundingBox()
aabb2 = ellipse.axisAlignedBoundingBox()
for prop in ['x', 'y', 'width', 'height']
expect(aabb1[prop]).toBe aabb2[prop]
it 'calculates ellipse intersections properly', ->
# ellipse with y major axis, off-origin center, and 45 degree rotation
ellipse = new Ellipse 1, 2, 4, 6, Math.PI / 4
expect(ellipse.intersectsShape new Rectangle(0, 0, 2, 2, 0)).toBe true
expect(ellipse.intersectsShape new Rectangle(0, -1, 2, 3, 0)).toBe true
expect(ellipse.intersectsShape new Rectangle(-1, -0.5, 2 * Math.SQRT2, 2 * Math.SQRT2, Math.PI / 4)).toBe true
expect(ellipse.intersectsShape new Rectangle(-1, -0.5, 2 * Math.SQRT2, 2 * Math.SQRT2, 0)).toBe true
expect(ellipse.intersectsShape new Rectangle(-1, -1, 2 * Math.SQRT2, 2 * Math.SQRT2, 0)).toBe true
expect(ellipse.intersectsShape new Rectangle(-1, -1, 2 * Math.SQRT2, 2 * Math.SQRT2, Math.PI / 4)).toBe false
expect(ellipse.intersectsShape new Rectangle(-2, -2, 2, 2, 0)).toBe false
expect(ellipse.intersectsShape new Rectangle(-Math.SQRT2 / 2, -Math.SQRT2 / 2, Math.SQRT2, Math.SQRT2, 0)).toBe false
expect(ellipse.intersectsShape new Rectangle(-Math.SQRT2 / 2, -Math.SQRT2 / 2, Math.SQRT2, Math.SQRT2, Math.PI / 4)).toBe false
expect(ellipse.intersectsShape new Rectangle(-2, 0, 2, 2, 0)).toBe false
expect(ellipse.intersectsShape new Rectangle(0, -2, 2, 2, 0)).toBe false
expect(ellipse.intersectsShape new Rectangle(1, 2, 1, 1, 0)).toBe true

View file

@ -28,13 +28,13 @@ describe 'LineSegment', ->
it 'can tell when a point is on a line or segment', ->
lineSegment = new LineSegment v00, v11
expect(lineSegment.pointOnLine v22, false).toBe true
#expect(lineSegment.pointOnLine v22, true).toBe false
#expect(lineSegment.pointOnLine v00, false).toBe true
#expect(lineSegment.pointOnLine v00, true).toBe true
#expect(lineSegment.pointOnLine v11, true).toBe true
#expect(lineSegment.pointOnLine v11, false).toBe true
#expect(lineSegment.pointOnLine v34, false).toBe false
#expect(lineSegment.pointOnLine v34, true).toBe false
expect(lineSegment.pointOnLine v22, true).toBe false
expect(lineSegment.pointOnLine v00, false).toBe true
expect(lineSegment.pointOnLine v00, true).toBe true
expect(lineSegment.pointOnLine v11, true).toBe true
expect(lineSegment.pointOnLine v11, false).toBe true
expect(lineSegment.pointOnLine v34, false).toBe false
expect(lineSegment.pointOnLine v34, true).toBe false
it 'correctly calculates distance to points', ->
lineSegment = new LineSegment v00, v11