PageRenderTime 26ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/templates/public/scripts/models/Participant.js

https://bitbucket.org/taxilian/racecontrol5
JavaScript | 564 lines | 531 code | 20 blank | 13 comment | 171 complexity | 4f3c21b2b3cdb2c53781869cad8f1272 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. define(["backbone", "underscore", "jquery", "models/Entry", "models/Station"],
  2. function(Backbone, _, $, Entry, Station) {
  3. var TIME_MISSING = Number.MAX_VALUE,
  4. Participant, Participants;
  5. Participant = Backbone.Model.extend({
  6. idAttribute: "bibNumber",
  7. url: function() {
  8. var url = "/newAPI/events/" + this.get("raceEvent_id") + "/participants";
  9. if (!this.isNew()) {
  10. url += "/" + this.id;
  11. }
  12. return url;
  13. },
  14. formatName: function() {
  15. return this.get("lastName") + ", " + this.get("firstName");
  16. },
  17. getEntrys: function() {
  18. var self = this;
  19. if (self.has("entrys")) {
  20. return self.get("entrys");
  21. }
  22. return Entry.list.fetchAll(self.collection.eventId, self.id).pipe(function(entrys) {
  23. self.set({entrys: entrys});
  24. return entrys;
  25. });
  26. },
  27. getRaceTime: function(startStation, station) {
  28. var entrys = this.getEntrys();
  29. if (entrys.promise) { // we dont' have entries yet
  30. return TIME_MISSING;
  31. }
  32. var eStart = entrys.get(startStation),
  33. eCur = entrys.get(station);
  34. if (!eStart || !eCur) {
  35. return TIME_MISSING;
  36. } else {
  37. var a = eCur.getValidTime();
  38. var b = eStart.getValidTime();
  39. if (!a || !b) {
  40. return TIME_MISSING;
  41. }
  42. return a-b;
  43. }
  44. },
  45. getOrCreateEntry: function(stationNo) {
  46. var self = this;
  47. return $.when(this.getEntrys()).pipe(function(entrys) {
  48. if (entrys.get(stationNo)) {
  49. return entrys.get(stationNo);
  50. } else {
  51. entrys.add({
  52. station_stationNumber: stationNo,
  53. participant_bibNumber: self.id,
  54. time_in: null,
  55. time_out: null
  56. });
  57. return entrys.get(stationNo);
  58. }
  59. });
  60. },
  61. calcTimeDelta: function(firstStation, secondStation) {
  62. var e1 = this.has("entrys") && this.get("entrys").get(firstStation);
  63. var e2 = this.has("entrys") && this.get("entrys").get(secondStation);
  64. if (!e1 || !e2) {
  65. return null;
  66. } else {
  67. var t1 = e1.get("time_out") || e1.get("time_in");
  68. var t2 = e2.get("time_in") || e2.get("time_out");
  69. if (!t1 || !t2) { return null; }
  70. // If we got this far, we have two times and we need the delta between them
  71. return t2-t1;
  72. }
  73. },
  74. getTimeInAt: function(stationNo, format) {
  75. var e = this.has("entrys") && this.get("entrys").get(stationNo);
  76. if (!e) {
  77. return null;
  78. } else {
  79. var t = e.get("time_in");
  80. if (format) {
  81. format = this.collection.raceEvent.get("finishStation") == stationNo ? "HH:mm:ss" : "HH:mm";
  82. return t && t.toString(format);
  83. } else {
  84. return t;
  85. }
  86. }
  87. },
  88. getTimeOutAt: function(stationNo, format) {
  89. var e = this.has("entrys") && this.get("entrys").get(stationNo);
  90. if (!e) {
  91. return null;
  92. } else {
  93. var t = e.get("time_out");
  94. if (format) {
  95. format = this.collection.raceEvent.get("finishStation") == stationNo ? "HH:mm:ss" : "HH:mm";
  96. return t && t.toString(format);
  97. } else {
  98. return t;
  99. }
  100. }
  101. },
  102. getTimesForGrid: function(stationNo) {
  103. var evt = this.collection.raceEvent,
  104. finishStation = evt.get("finishStation");
  105. var e = this.has("entrys") && this.get("entrys").get(stationNo);
  106. if (!e) {
  107. return [null, null];
  108. } else {
  109. return [e.getGridTimeIn(finishStation == stationNo), e.getGridTimeOut()];
  110. }
  111. },
  112. getTimes: function(stationNo) {
  113. var e = this.has("entrys") && this.get("entrys").get(stationNo);
  114. if (!e) {
  115. return [null, null];
  116. } else {
  117. return [e.get("time_in"), e.get("time_out")];
  118. }
  119. },
  120. isDroppedAt: function(stationNo) {
  121. return (this.get("dnfStation") == stationNo);
  122. },
  123. isDroppedBy: function(stationNo) {
  124. var dnfStation = this.get("dnfStation");
  125. if (dnfStation || dnfStation === 0) {
  126. return dnfStation <= stationNo;
  127. } else { return false; }
  128. },
  129. getLastSeenAt: function() {
  130. if (!this.has("entrys")) {
  131. return false;
  132. } else {
  133. return this.get("entrys").max(function(e) { return e.hasValidTime() ? e.get("station_stationNumber") : false; });
  134. }
  135. },
  136. isExpectedAt: function(stationNo) {
  137. var evt = this.collection.raceEvent,
  138. startStation = evt.get("startStation");
  139. if (this.isDropped() || !this.has("entrys")) {
  140. return false;
  141. }
  142. var entrys = this.get("entrys");
  143. if (!entrys.get(startStation)) {
  144. return false;
  145. } else {
  146. return !entrys.any(function(e) {
  147. return e.get("station_stationNumber") >= stationNo && e.hasValidTime();
  148. });
  149. }
  150. },
  151. wasMissedAt: function(stationNo) {
  152. var evt = this.collection.raceEvent,
  153. startStation = evt.get("startStation");
  154. if (!this.has("entrys")) {
  155. // To be "missed" you must have started the race
  156. return false;
  157. }
  158. var entrys = this.get("entrys");
  159. var cEntry = entrys.get(stationNo);
  160. if (cEntry && cEntry.hasValidTime()) {
  161. // They have been seen at the given station so they weren't missed
  162. return false;
  163. } else if (this.get("dnfStation") && this.get("dnfStation") > stationNo) {
  164. return true;
  165. } else {
  166. return entrys.any(function(e) {
  167. return e.get("station_stationNumber") >= stationNo && e.hasValidTime();
  168. });
  169. }
  170. },
  171. hasBadDataAt: function(stationNo) {
  172. var curEntry = this.getStationEntry(stationNo),
  173. prevEntry = curEntry && curEntry.prevStationEntry,
  174. curIn = curEntry && curEntry.timeIn,
  175. curOut = curEntry && curEntry.timeOut,
  176. prevIn = prevEntry && prevEntry.timeIn,
  177. prevOut = prevEntry && prevEntry.timeOut,
  178. // when comparing to the previous station, prefer current in over current out
  179. // but previous out over previous in
  180. curStationTime = curIn || curOut,
  181. prevStationTime = prevOut || prevIn;
  182. // "bad data" means that the time out is earlier than the time in,
  183. // or the time at this station is BEFORE the time at the previous station.
  184. if ((curOut && curIn && curOut < curIn) || (curStationTime && prevStationTime && curStationTime <= prevStationTime)) {
  185. return true;
  186. }
  187. // if we didn't find bad data, it must be good!
  188. return false;
  189. },
  190. wasSeenAt: function(stationNo) {
  191. if (!this.has("entrys")) {
  192. return false;
  193. }
  194. var entry = this.get("entrys").get(stationNo);
  195. return !!(entry && entry.hasValidTime());
  196. },
  197. isDropped: function() {
  198. return (this.get("dnfStation") || this.get("dnfStation") === 0);
  199. },
  200. getEventId: function() {
  201. return this.collection.eventId;
  202. },
  203. getSex: function() {
  204. // This is ugly, but we'll default to assuming "male" unless known otherwise
  205. return String(this.get("sex")).toUpperCase() == "F" ? "F" : "M";
  206. },
  207. validate: function(attrs) {
  208. var self = this;
  209. var col = this.collection;
  210. attrs.bibNumber = Number(attrs.bibNumber || this.id);
  211. attrs.age = Number(attrs.age || this.get("age"));
  212. if ("firstName" in attrs && !attrs.firstName) {
  213. return "Please enter a first name";
  214. } else if ("lastName" in attrs && !attrs.lastName) {
  215. return "Please enter a last name";
  216. }
  217. if ("entrys" in attrs) {
  218. if (this.get("entrys")) {
  219. this.get("entrys").unbind("change");
  220. }
  221. attrs.entrys.bind("change", function(entry) { self.trigger("change", self, entry); });
  222. }
  223. },
  224. clearPositionDelta: function() {
  225. this._prevStation = null;
  226. },
  227. setPosition: function(sNo, position, distance, calcDelta) {
  228. var self = this;
  229. if (!self.positionData) {
  230. self.positionData = {};
  231. }
  232. var pdata = self.positionData,
  233. prevStation = self._prevStation,
  234. prevDistance = self._prevDistance,
  235. delta = null,
  236. timeDelta = null,
  237. distDelta = null;
  238. if (prevStation !== null && calcDelta && !self.wasMissedAt(prevStation)) {
  239. delta = self.getPosition(prevStation);
  240. timeDelta = self.calcTimeDelta(prevStation, sNo);
  241. if (delta || delta === 0) {
  242. delta = position - delta;
  243. }
  244. if (prevDistance !== null) {
  245. distDelta = parseFloat(distance) - prevDistance;
  246. }
  247. }
  248. if (calcDelta) {
  249. self._prevStation = sNo;
  250. self._prevDistance = parseFloat(distance) || null;
  251. }
  252. pdata[sNo] = {
  253. pos: position,
  254. delta: delta,
  255. timeDelta: timeDelta,
  256. distDelta: distDelta || null
  257. };
  258. },
  259. getPosition: function(sNo) {
  260. if (this.positionData && this.positionData[sNo]) {
  261. var p = this.positionData[sNo].pos;
  262. if (p || p === 0) {
  263. return p;
  264. } else {
  265. return "N/A";
  266. }
  267. } else {
  268. return null;
  269. }
  270. },
  271. getPositionDelta: function(sNo) {
  272. if (this.positionData && this.positionData[sNo]) {
  273. var d = this.positionData[sNo].delta;
  274. if (d || d === 0) {
  275. return d;
  276. } else {
  277. return "N/A";
  278. }
  279. }
  280. },
  281. clearStationEntries : function() {
  282. this.stationEntries = {};
  283. this._prevStationEntry = null;
  284. },
  285. setStationEntry: function(sNo) {
  286. var timeIn = this.getTimeInAt(sNo),
  287. timeOut = this.getTimeOutAt(sNo),
  288. stationEntry = (timeIn || timeOut) ? {timeIn : timeIn, timeOut: timeOut, prevStationEntry: this._prevStationEntry} : null;
  289. if (stationEntry) {
  290. this.stationEntries[sNo] = this._prevStationEntry = stationEntry;
  291. }
  292. },
  293. getStationEntry: function(sNo) {
  294. return this.stationEntries && this.stationEntries[sNo];
  295. },
  296. isFallingBehind: function(sNo, scale) {
  297. var t = this.getPositionDelta(sNo);
  298. if (!t || t == "N/A") { return; }
  299. var d = t / scale;
  300. if (d < 0) {
  301. return 0;
  302. } else if (d > 10) {
  303. return 10;
  304. } else {
  305. return Math.floor(d);
  306. }
  307. },
  308. getPace: function(sNo) {
  309. if (this.positionData && this.positionData[sNo]) {
  310. var pos = this.positionData[sNo].timeDelta;
  311. var hour = pos / 1000 / 60 / 60;
  312. var dist = this.positionData[sNo].distDelta;
  313. if (pos || pos === 0 && dist) {
  314. return Math.floor(dist / hour * 10) / 10;
  315. } else {
  316. return null;
  317. }
  318. }
  319. return null;
  320. },
  321. getTimeDelta: function(sNo) {
  322. if (this.positionData && this.positionData[sNo]) {
  323. var d = this.positionData[sNo].timeDelta;
  324. if (d || d === 0) {
  325. return d;
  326. } else {
  327. return null;
  328. }
  329. }
  330. return null;
  331. },
  332. getFormattedTimeDelta: function(sNo) {
  333. var td = this.getTimeDelta(sNo);
  334. if (td === null) { return ""; }
  335. var seconds = td / 1000;
  336. var hours = Math.floor(seconds / 60 / 60);
  337. var minutes = Math.floor(seconds / 60) % 60;
  338. seconds = Math.floor(seconds % 60);
  339. hours += "";
  340. minutes += "";
  341. seconds += "";
  342. if (minutes.length == 1) { minutes = '0' + minutes; }
  343. if (seconds.length == 1) { seconds = '0' + seconds; }
  344. return hours + ":" + minutes + ":" + seconds;
  345. }
  346. }, {
  347. fetchById: function(event_id, id) {
  348. return $.when(Participants.fetchAll(event_id)).pipe(function(list) {
  349. return list.get(id);
  350. });
  351. },
  352. swap: function(eventId, source, dest) {
  353. var data = {
  354. source: source, dest: dest
  355. };
  356. var params = {
  357. "url": "/newAPI/events/" + eventId + "/participants/swap",
  358. "data": data,
  359. "type": "POST"
  360. };
  361. return $.ajax(params);
  362. }
  363. });
  364. Participants = Backbone.Collection.extend({
  365. model: Participant,
  366. url: function() {
  367. return "/newAPI/events/" + this.eventId + "/participants";
  368. },
  369. calculateResults: function() {
  370. var self = this;
  371. self.each(function(r) {
  372. r.clearPositionDelta();
  373. r.clearStationEntries();
  374. });
  375. // Be sure this never gets called before RaceEvent has finished loading
  376. var raceEvent = this.raceEvent;
  377. var startStation = raceEvent.get("startStation");
  378. var finishStation = raceEvent.get("finishStation");
  379. if ((!startStation && startStation !== 0) ||
  380. (!finishStation && finishStation !== 0)) {
  381. return new $.Deferred().reject("No start/finish station"); // Can't calculate race results without a start and finish station
  382. }
  383. return $.when(Station.list.fetchAll(self.eventId)).pipe(function(stations) {
  384. // Now we need to loop over the stations and calculate positions
  385. var results = {};
  386. stations.each(function(station) {
  387. var sNo = station.id;
  388. function sortFunc(ao, bo) {
  389. var a = ao.getRaceTime(startStation, sNo);
  390. var b = bo.getRaceTime(startStation, sNo);
  391. if (a == b) { return 0; }
  392. else { return a > b ? 1 : -1; }
  393. }
  394. results[sNo] = self.toArray();
  395. results[sNo].sort(sortFunc);
  396. });
  397. _.each(results, function(sortedList, sNo) {
  398. if (sNo == startStation) {
  399. return;
  400. }
  401. var last = TIME_MISSING, place = 1, nextplace = 0;
  402. _.each(sortedList, function(runner, position) {
  403. var rtime = runner.getRaceTime(startStation, sNo);
  404. if (runner.isDroppedBy(sNo)) {
  405. runner.setPosition(sNo, "DNF", stations.get(sNo).get('distance'));
  406. } else {
  407. if (last == rtime) {
  408. ++nextplace;
  409. } else {
  410. ++nextplace;
  411. place = nextplace;
  412. }
  413. runner.setStationEntry(sNo);
  414. runner.setPosition(sNo, place, stations.get(sNo).get('distance'), true);
  415. last = rtime;
  416. }
  417. if (sNo == finishStation) {
  418. runner.time = rtime;
  419. runner.place = runner.getPosition(sNo);
  420. }
  421. });
  422. });
  423. });
  424. },
  425. calculateResults_old: function() {
  426. var self = this;
  427. // Be sure this never gets called before RaceEvent has finished loading
  428. var raceEvent = this.raceEvent;
  429. var startStation = raceEvent.get("startStation");
  430. var finishStation = raceEvent.get("finishStation");
  431. if ((!startStation && startStation !== 0) ||
  432. (!finishStation && finishStation !== 0)) {
  433. return; // Can't calculate race results without a start and finish station
  434. }
  435. self.each(function(runner) {
  436. if (!runner.has("entrys")) {
  437. runner.time = TIME_MISSING;
  438. } else {
  439. var start_time = runner.getTimeOutAt(startStation);
  440. var end_time = runner.getTimeInAt(finishStation);
  441. if (!start_time || !end_time) {
  442. runner.time = TIME_MISSING;
  443. } else {
  444. runner.time = end_time - start_time;
  445. }
  446. }
  447. });
  448. var presort = self.toArray();
  449. function sortFunc(ao, bo) {
  450. var a = ao.time, b = bo.time;
  451. if (a == b) { return 0; }
  452. else { return a > b ? 1 : -1; }
  453. }
  454. var last = TIME_MISSING, place = 1, nextplace = 0;
  455. presort.sort(sortFunc);
  456. _.each(presort, function(runner) {
  457. if (runner.isDropped()) {
  458. runner.place = "DNF";
  459. } else if (runner.time == TIME_MISSING) {
  460. runner.place = null;
  461. } else {
  462. if (last == runner.time) {
  463. ++nextplace;
  464. } else {
  465. ++nextplace;
  466. place = nextplace;
  467. }
  468. runner.place = place;
  469. last = runner.time;
  470. }
  471. });
  472. return true;
  473. },
  474. getSortedList: function(sortFunc) {
  475. var arr = this.toArray();
  476. arr.sort(sortFunc);
  477. return arr;
  478. },
  479. getEntrys: function() {
  480. var self=this, ids = self.pluck("bibNumber"), numComplete = 0;
  481. if (self.getEntrysDfd) {
  482. return self.getEntrysDfd;
  483. }
  484. function downloadEntryList(elist) {
  485. var Entry = require("models/Entry");
  486. var params = {
  487. "url": "/newAPI/events/" + self.eventId + "/entrys/range",
  488. "contentType": "application/json",
  489. "data": JSON.stringify(elist),
  490. "type": "POST"
  491. };
  492. return $.ajax(params).pipe(function(res) {
  493. var map = {};
  494. numComplete += elist.length;
  495. _.each(res, function(entry) {
  496. if (!map[entry.participant_bibNumber]) { map[entry.participant_bibNumber] = []; }
  497. map[entry.participant_bibNumber].push(entry);
  498. });
  499. _.each(map, function(list, bibNumber) {
  500. var nl = new (Entry.list)(Entry.list.parse(list));
  501. nl.runnerNo = bibNumber;
  502. nl.eventId = self.eventId;
  503. self.get(bibNumber).set({entrys: nl});
  504. });
  505. self.trigger("entryDownload", numComplete, self.length);
  506. return true;
  507. });
  508. }
  509. var i,j,dfdResults=[],temparray,chunk = 25;
  510. for (i=0,j=ids.length; i<j; i+=chunk) {
  511. temparray = ids.slice(i,i+chunk);
  512. dfdResults.push(downloadEntryList(temparray));
  513. }
  514. self.getEntrysDfd = $.when.apply(null, dfdResults);
  515. return self.getEntrysDfd;
  516. }
  517. }, {
  518. list: null,
  519. fetchAll: function(eventId) {
  520. var self = this, cache;
  521. if (!self.list) { self.list = {}; }
  522. cache = self.list[eventId];
  523. if (cache) {
  524. return cache;
  525. }
  526. var tmp = new self();
  527. tmp.eventId = eventId;
  528. cache = self.list[eventId] = tmp.fetch().pipe(function() {
  529. // We end up needing the RaceEvent object often enough it's worth cacheing here
  530. var RaceEvent = require("models/RaceEvent");
  531. self.list[eventId] = tmp;
  532. return RaceEvent.obj.fetchById(eventId).pipe(function(raceEvent) {
  533. tmp.raceEvent = raceEvent;
  534. return tmp;
  535. });
  536. });
  537. return cache;
  538. }
  539. });
  540. return {
  541. obj: Participant,
  542. list: Participants
  543. };
  544. });