-
Notifications
You must be signed in to change notification settings - Fork 9
/
splitsbrowser.js
12797 lines (11251 loc) · 511 KB
/
splitsbrowser.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*!
* SplitsBrowser - Orienteering results analysis.
*
* Copyright (C) 2000-2022 Dave Ryder, Reinhard Balling, Andris Strazdins,
* Ed Nash, Luke Woodward
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
/*
* SplitsBrowser Core - Namespaces the rest of the program depends on.
*
* Copyright (C) 2000-2022 Dave Ryder, Reinhard Balling, Andris Strazdins,
* Ed Nash, Luke Woodward
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
// Tell ESLint not to complain that this is redeclaring a constant.
/* eslint no-redeclare: "off", no-unused-vars: "off" */
var SplitsBrowser = { Version: "3.5.4", Model: {}, Input: {}, Controls: {}, Messages: {} };
/*
* SplitsBrowser - Assorted utility functions.
*
* Copyright (C) 2000-2013 Dave Ryder, Reinhard Balling, Andris Strazdins,
* Ed Nash, Luke Woodward
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
(function () {
"use strict";
// Minimum length of a course that is considered to be given in metres as
// opposed to kilometres.
var MIN_COURSE_LENGTH_METRES = 500;
/**
* Utility function used with filters that simply returns the object given.
* @param {any} x Any input value
* @return {any} The input value.
*/
SplitsBrowser.isTrue = function (x) { return x; };
/**
* Utility function that returns whether a value is not null.
* @param {any} x Any input value.
* @return {Boolean} True if the value is not null, false otherwise.
*/
SplitsBrowser.isNotNull = function (x) { return x !== null; };
/**
* Returns whether the value given is the numeric value NaN.
*
* This differs from the JavaScript built-in function isNaN, in that isNaN
* attempts to convert the value to a number first, with non-numeric strings
* being converted to NaN. So isNaN("abc") will be true, even though "abc"
* isn't NaN. This function only returns true if you actually pass it NaN,
* rather than any value that fails to convert to a number.
*
* @param {any} x Any input value.
* @return {Boolean} True if x is NaN, false if x is any other value.
*/
SplitsBrowser.isNaNStrict = function (x) { return x !== x; };
/**
* Returns whether the value given is neither null nor NaN.
* @param {Number|null} x A value to test.
* @return {Boolean} False if the value given is null or NaN, true
* otherwise.
*/
SplitsBrowser.isNotNullNorNaN = function (x) { return x !== null && x === x; };
/**
* Exception object raised if invalid data is passed.
* @constructor
* @param {String} message The exception detail message.
*/
function InvalidData(message) {
this.name = "InvalidData";
this.message = message;
}
/**
* Returns a string representation of this exception.
* @return {String} String representation.
*/
InvalidData.prototype.toString = function () {
return this.name + ": " + this.message;
};
/**
* Utility function to throw an 'InvalidData' exception object.
* @param {String} message The exception message.
* @throws {InvalidData} if invoked.
*/
SplitsBrowser.throwInvalidData = function (message) {
throw new InvalidData(message);
};
/**
* Exception object raised if a data parser for a format deems that the data
* given is not of that format.
* @constructor
* @param {String} message The exception message.
*/
function WrongFileFormat(message) {
this.name = "WrongFileFormat";
this.message = message;
}
/**
* Returns a string representation of this exception.
* @return {String} String representation.
*/
WrongFileFormat.prototype.toString = function () {
return this.name + ": " + this.message;
};
/**
* Utility function to throw a 'WrongFileFormat' exception object.
* @param {String} message The exception message.
* @throws {WrongFileFormat} if invoked.
*/
SplitsBrowser.throwWrongFileFormat = function (message) {
throw new WrongFileFormat(message);
};
/**
* Checks whether the given object contains a property with the given name.
* This is a wrapper around the call to Object.prototype.hasOwnProperty.
* @param {Object} object The object to test.
* @param {String} property The name of the property.
* @return {Boolean} Whether the object has a property with the given name.
*/
SplitsBrowser.hasProperty = function (object, property) {
return Object.prototype.hasOwnProperty.call(object, property);
};
/**
* Returns the sum of two numbers, or null if either is null.
* @param {Number|null} a One number, or null, to add.
* @param {Number|null} b The other number, or null, to add.
* @return {Number|null} null if at least one of a or b is null,
* otherwise a + b.
*/
SplitsBrowser.addIfNotNull = function (a, b) {
return (a === null || b === null) ? null : (a + b);
};
/**
* Returns the difference of two numbers, or null if either is null.
* @param {Number|null} a One number, or null, to add.
* @param {Number|null} b The other number, or null, to add.
* @return {Number|null} null if at least one of a or b is null,
* otherwise a - b.
*/
SplitsBrowser.subtractIfNotNull = function (a, b) {
return (a === null || b === null) ? null : (a - b);
};
/**
* Parses a course length.
*
* This can be specified as a decimal number of kilometres or metres, with
* either a full stop or a comma as the decimal separator.
*
* @param {String} stringValue The course length to parse, as a string.
* @return {Number|null} The parsed course length, or null if not valid.
*/
SplitsBrowser.parseCourseLength = function (stringValue) {
var courseLength = parseFloat(stringValue.replace(",", "."));
if (!isFinite(courseLength)) {
return null;
}
if (courseLength >= MIN_COURSE_LENGTH_METRES) {
courseLength /= 1000;
}
return courseLength;
};
/**
* Parses a course climb, specified as a whole number of metres.
*
* @param {String} stringValue The course climb to parse, as a string.
* @return {Number|null} The parsed course climb, or null if not valid.
*/
SplitsBrowser.parseCourseClimb = function (stringValue) {
var courseClimb = parseInt(stringValue, 10);
if (SplitsBrowser.isNaNStrict(courseClimb)) {
return null;
} else {
return courseClimb;
}
};
/**
* Normalise line endings so that all lines end with LF, instead of
* CRLF or CR.
* @param {String} stringValue The string value to normalise line endings
* within.
* @return {String} String value with the line-endings normalised.
*/
SplitsBrowser.normaliseLineEndings = function (stringValue) {
return stringValue.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
};
})();
/*
* SplitsBrowser Messages - Fetches internationalised message strings.
*
* Copyright (C) 2000-2020 Dave Ryder, Reinhard Balling, Andris Strazdins,
* Ed Nash, Luke Woodward.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
(function () {
"use strict";
var hasProperty = SplitsBrowser.hasProperty;
// Whether a warning about missing messages has been given. We don't
// really want to irritate the user with many alert boxes if there's a
// problem with the messages.
var warnedAboutMessages = false;
// Default alerter function, just calls window.alert.
var alertFunc = function (message) { window.alert(message); };
// The currently-chosen language, or null if none chosen or found yet.
var currentLanguage = null;
// The list of all languages read in, or null if none.
var allLanguages = null;
// The messages object.
var messages = SplitsBrowser.Messages;
/**
* Issue a warning about the messages, if a warning hasn't already been
* issued.
* @param {String} warning The warning message to issue.
*/
function warn(warning) {
if (!warnedAboutMessages) {
alertFunc(warning);
warnedAboutMessages = true;
}
}
/**
* Sets the alerter to use when a warning message should be shown.
*
* This function is intended only for testing purposes.
* @param {Function} alerter The function to be called when a warning is
* to be shown.
*/
SplitsBrowser.setMessageAlerter = function (alerter) {
alertFunc = alerter;
};
/**
* Attempts to get a message, returning a default string if it does not
* exist.
* @param {String} key The key of the message.
* @param {String} defaultValue Value to be used
* @return {String} The message with the given key, if the key exists,
* otherwise the default value.
*/
SplitsBrowser.tryGetMessage = function (key, defaultValue) {
return (currentLanguage !== null && hasProperty(messages[currentLanguage], key)) ? SplitsBrowser.getMessage(key) : defaultValue;
};
/**
* Returns the message with the given key.
* @param {String} key The key of the message.
* @return {String} The message with the given key, or a placeholder string
* if the message could not be looked up.
*/
SplitsBrowser.getMessage = function (key) {
if (allLanguages === null) {
SplitsBrowser.initialiseMessages();
}
if (currentLanguage !== null) {
if (hasProperty(messages[currentLanguage], key)) {
return messages[currentLanguage][key];
} else {
warn("Message not found for key '" + key + "' in language '" + currentLanguage + "'");
return "?????";
}
} else {
warn("No messages found. Has a language file been loaded?");
return "?????";
}
};
/**
* Returns the message with the given key, with some string formatting
* applied to the result.
*
* The object 'params' should map search strings to their replacements.
*
* @param {String} key The key of the message.
* @param {Object} params Object mapping parameter names to values.
* @return {String} The resulting message.
*/
SplitsBrowser.getMessageWithFormatting = function (key, params) {
var message = SplitsBrowser.getMessage(key);
for (var paramName in params) {
if (hasProperty(params, paramName)) {
// Irritatingly there isn't a way of doing global replace
// without using regexps. So we must escape any magic regex
// metacharacters first, so that we have a regexp that will
// match a single static string.
var paramNameRegexEscaped = paramName.replace(/([.+*?|{}()^$[\]\\])/g, "\\$1");
message = message.replace(new RegExp(paramNameRegexEscaped, "g"), params[paramName]);
}
}
return message;
};
/**
* Returns an array of codes of languages that have been loaded.
* @return {Array} Array of language codes.
*/
SplitsBrowser.getAllLanguages = function () {
return allLanguages.slice(0);
};
/**
* Returns the language code of the current language, e.g. "en_gb".
* @return {String} Language code of the current language.
*/
SplitsBrowser.getLanguage = function () {
return currentLanguage;
};
/**
* Returns the name of the language with the given code.
* @param {String} language The code of the language, e.g. "en_gb".
* @return {String} The name of the language, e.g. "English".
*/
SplitsBrowser.getLanguageName = function (language) {
if (hasProperty(messages, language) && hasProperty(messages[language], "Language")) {
return messages[language].Language;
} else {
return "?????";
}
};
/**
* Sets the current language.
* @param {String} language The code of the new language to set.
*/
SplitsBrowser.setLanguage = function (language) {
if (hasProperty(messages, language)) {
currentLanguage = language;
}
};
/**
* Initialises the messages from those read in.
*
* @param {String} defaultLanguage (Optional) The default language to choose.
*/
SplitsBrowser.initialiseMessages = function (defaultLanguage) {
allLanguages = [];
if (messages !== SplitsBrowser.Messages) {
// SplitsBrowser.Messages has changed since the JS source was
// loaded and now. Likely culprit is an old-format language file.
warn("You appear to have loaded a messages file in the old format. This file, and all " +
"others loaded after it, will not work.\n\nPlease check the messages files.");
}
for (var messageKey in messages) {
if (hasProperty(messages, messageKey)) {
allLanguages.push(messageKey);
}
}
if (allLanguages.length === 0) {
warn("No messages files were found.");
} else if (defaultLanguage && hasProperty(messages, defaultLanguage)) {
currentLanguage = defaultLanguage;
} else {
currentLanguage = allLanguages[0];
}
};
})();
/*
* SplitsBrowser Time - Functions for time handling and conversion.
*
* Copyright (C) 2000-2022 Dave Ryder, Reinhard Balling, Andris Strazdins,
* Ed Nash, Luke Woodward
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
(function () {
"use strict";
SplitsBrowser.NULL_TIME_PLACEHOLDER = "-----";
var isNaNStrict = SplitsBrowser.isNaNStrict;
/**
* Formats a number to two digits, preceding it with a zero if necessary,
* e.g. 47 -> "47", 8 -> "08".
* @param {Number} value The value to format.
* @return {String} Number formatted with a leading zero if necessary.
*/
function formatToTwoDigits(value) {
return (value < 10) ? "0" + value : value.toString();
}
/**
* Formats a time period given as a number of seconds as a string in the form
* [-][h:]mm:ss.ss .
* @param {Number} seconds The number of seconds.
* @param {Number|null} precision Optional number of decimal places to format
* using, or the default if not specified.
* @return {String} The string formatting of the time.
*/
SplitsBrowser.formatTime = function (seconds, precision) {
if (seconds === null) {
return SplitsBrowser.NULL_TIME_PLACEHOLDER;
} else if (isNaNStrict(seconds)) {
return "???";
}
var result = "";
if (seconds < 0) {
result = "-";
seconds = -seconds;
}
var hours = Math.floor(seconds / (60 * 60));
var mins = Math.floor(seconds / 60) % 60;
var secs = seconds % 60;
if (hours > 0) {
result += hours.toString() + ":";
}
result += formatToTwoDigits(mins) + ":";
if (secs < 10) {
result += "0";
}
if (typeof precision === "number") {
result += secs.toFixed(precision);
} else {
result += Math.round(secs * 100) / 100;
}
return result;
};
/**
* Formats a number of seconds as a time of day. This returns a string
* of the form HH:MM:SS, with HH no more than 24.
* @param {Number} seconds The number of seconds
* @return {String} The time of day formatted as a string.
*/
SplitsBrowser.formatTimeOfDay = function (seconds) {
var hours = Math.floor((seconds / (60 * 60)) % 24);
var mins = Math.floor(seconds / 60) % 60;
var secs = Math.floor(seconds % 60);
return formatToTwoDigits(hours) + ":" + formatToTwoDigits(mins) + ":" + formatToTwoDigits(secs);
};
/**
* Parse a time of the form MM:SS or H:MM:SS into a number of seconds.
* @param {String} time The time of the form MM:SS.
* @return {Number|null} The number of seconds.
*/
SplitsBrowser.parseTime = function (time) {
time = time.trim();
if (/^(-?\d+:)?-?\d+:-?\d\d([,.]\d{1,10})?$/.test(time)) {
var timeParts = time.replace(",", ".").split(":");
var totalTime = 0;
timeParts.forEach(function (timePart) {
totalTime = totalTime * 60 + parseFloat(timePart);
});
return totalTime;
} else {
// Assume anything unrecognised is a missed split.
return null;
}
};
})();
/*
* SplitsBrowser Result - The results for a competitor or a team.
*
* Copyright (C) 2000-2022 Dave Ryder, Reinhard Balling, Andris Strazdins,
* Ed Nash, Luke Woodward
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
(function () {
"use strict";
var NUMBER_TYPE = typeof 0;
var isNotNull = SplitsBrowser.isNotNull;
var isNaNStrict = SplitsBrowser.isNaNStrict;
var hasProperty = SplitsBrowser.hasProperty;
var addIfNotNull = SplitsBrowser.addIfNotNull;
var subtractIfNotNull = SplitsBrowser.subtractIfNotNull;
var throwInvalidData = SplitsBrowser.throwInvalidData;
/**
* Function used with the JavaScript sort method to sort results in order.
*
* Results that are mispunched are sorted to the end of the list.
*
* The return value of this method will be:
* (1) a negative number if result a comes before result b,
* (2) a positive number if result a comes after result a,
* (3) zero if the order of a and b makes no difference (i.e. they have the
* same total time, or both mispunched.)
*
* @param {SplitsBrowser.Model.Result} a One result to compare.
* @param {SplitsBrowser.Model.Result} b The other result to compare.
* @return {Number} Result of comparing two results.
*/
SplitsBrowser.Model.compareResults = function (a, b) {
if (a.isDisqualified !== b.isDisqualified) {
return (a.isDisqualified) ? 1 : -1;
} else if (a.totalTime === b.totalTime) {
return a.order - b.order;
} else if (a.totalTime === null) {
return (b.totalTime === null) ? 0 : 1;
} else {
return (b.totalTime === null) ? -1 : a.totalTime - b.totalTime;
}
};
/**
* Convert an array of cumulative times into an array of split times.
* If any null cumulative splits are given, the split times to and from that
* control are null also.
*
* The input array should begin with a zero, for the cumulative time to the
* start.
* @param {Array} cumTimes Array of cumulative split times.
* @return {Array} Corresponding array of split times.
*/
function splitTimesFromCumTimes(cumTimes) {
if (!$.isArray(cumTimes)) {
throw new TypeError("Cumulative times must be an array - got " + typeof cumTimes + " instead");
} else if (cumTimes.length === 0) {
throwInvalidData("Array of cumulative times must not be empty");
} else if (cumTimes[0] !== 0) {
throwInvalidData("Array of cumulative times must have zero as its first item");
} else if (cumTimes.length === 1) {
throwInvalidData("Array of cumulative times must contain more than just a single zero");
}
var splitTimes = [];
for (var i = 0; i + 1 < cumTimes.length; i += 1) {
splitTimes.push(subtractIfNotNull(cumTimes[i + 1], cumTimes[i]));
}
return splitTimes;
}
/**
* Object that represents the data for a single competitor or team.
*
* The first parameter (order) merely stores the order in which the competitor
* or team appears in the given list of results. Its sole use is to stabilise
* sorts of competitors or teams, as JavaScript's sort() method is not
* guaranteed to be a stable sort. However, it is not strictly the finishing
* order of the competitors, as it has been known for them to be given not in
* the correct order.
*
* The split and cumulative times passed here should be the 'original' times,
* before any attempt is made to repair the data.
*
* It is not recommended to use this constructor directly. Instead, use one
* of the factory methods fromCumTimes or fromOriginalCumTimes to pass in
* either the split or cumulative times and have the other calculated.
*
* @constructor
* @param {Number} order The order of the result.
* @param {Number|null} startTime The start time of the competitor or team, in
* seconds past midnight
* @param {Array} originalSplitTimes Array of split times, as numbers,
* with nulls for missed controls.
* @param {Array} originalCumTimes Array of cumulative times, as
* numbers, with nulls for missed controls.
& @param {Object} owner The competitor or team that recorded this result.
*/
function Result(order, startTime, originalSplitTimes, originalCumTimes, owner) {
if (typeof order !== NUMBER_TYPE) {
throwInvalidData("Result order must be a number, got " + typeof order + " '" + order + "' instead");
}
if (typeof startTime !== NUMBER_TYPE && startTime !== null) {
throwInvalidData("Start time must be a number, got " + typeof startTime + " '" + startTime + "' instead");
}
this.order = order;
this.startTime = startTime;
this.owner = owner;
this.isOKDespiteMissingTimes = false;
this.isNonCompetitive = false;
this.isNonStarter = false;
this.isNonFinisher = false;
this.isDisqualified = false;
this.isOverMaxTime = false;
this.originalSplitTimes = originalSplitTimes;
this.originalCumTimes = originalCumTimes;
this.splitTimes = null;
this.cumTimes = null;
this.splitRanks = null;
this.cumRanks = null;
this.timeLosses = null;
this.className = null;
this.offsets = null;
this.totalTime = (originalCumTimes === null || originalCumTimes.indexOf(null) > -1) ? null : originalCumTimes[originalCumTimes.length - 1];
}
/**
* Marks this result as having completed the course despite having missing times.
*/
Result.prototype.setOKDespiteMissingTimes = function () {
this.isOKDespiteMissingTimes = true;
if (this.originalCumTimes !== null) {
this.totalTime = this.originalCumTimes[this.originalCumTimes.length - 1];
}
};
/**
* Marks this result as non-competitive.
*/
Result.prototype.setNonCompetitive = function () {
this.isNonCompetitive = true;
};
/**
* Marks this result as not starting.
*/
Result.prototype.setNonStarter = function () {
this.isNonStarter = true;
};
/**
* Marks this result as not finishing.
*/
Result.prototype.setNonFinisher = function () {
this.isNonFinisher = true;
};
/**
* Marks this result as disqualified, for reasons other than a missing
* punch.
*/
Result.prototype.disqualify = function () {
this.isDisqualified = true;
};
/**
* Marks this result as over maximum time.
*/
Result.prototype.setOverMaxTime = function () {
this.isOverMaxTime = true;
};
/**
* Sets the name of the class that the result belongs to.
* This is the course-class, not the result's age class.
* @param {String} className The name of the class.
*/
Result.prototype.setClassName = function (className) {
this.className = className;
};
/**
* Sets the control offsets of the various competitors that make up the team.
* offsets[legIndex] should be the index of the start control of the competitor
* who ran in leg 'legIndex'.
* @param {Array} offsets The control offsets of the competitors.
*/
Result.prototype.setOffsets = function (offsets) {
this.offsets = offsets;
};
/**
* Create and return a Result object where the times are given as a list of
* cumulative times.
*
* This method does not assume that the data given has been 'repaired'. This
* function should therefore be used to create a result if the data may later
* need to be repaired.
*
* @param {Number} order The order of the result.
* @param {Number|null} startTime The start time, as seconds past midnight.
* @param {Array} cumTimes Array of cumulative split times, as numbers, with
* nulls for missed controls.
& @param {Object} owner The competitor or team that recorded this result.
* @return {Result} Created result.
*/
Result.fromOriginalCumTimes = function (order, startTime, cumTimes, owner) {
var splitTimes = splitTimesFromCumTimes(cumTimes);
return new Result(order, startTime, splitTimes, cumTimes, owner);
};
/**
* Create and return a Result object where the times are given as a list of
* cumulative times.
*
* This method assumes that the data given has been repaired, so it is ready
* to be viewed.
*
* @param {Number} order The order of the result.
* @param {Number|null} startTime The start time, as seconds past midnight.
* @param {Array} cumTimes Array of cumulative split times, as numbers, with
* nulls for missed controls.
& @param {Object} owner The competitor or team that recorded this result.
* @return {Result} Created result.
*/
Result.fromCumTimes = function (order, startTime, cumTimes, owner) {
var result = Result.fromOriginalCumTimes(order, startTime, cumTimes, owner);
result.splitTimes = result.originalSplitTimes;
result.cumTimes = result.originalCumTimes;
return result;
};
/**
* Sets the 'repaired' cumulative times. This also calculates the repaired
* split times.
* @param {Array} cumTimes The 'repaired' cumulative times.
*/
Result.prototype.setRepairedCumulativeTimes = function (cumTimes) {
this.cumTimes = cumTimes;
this.splitTimes = splitTimesFromCumTimes(cumTimes);
};
/**
* Returns whether this result indicated the competitor or team completed the
* course and did not get
* disqualified.
* @return {Boolean} True if the competitor or team completed the course and
* did not get disqualified, false if they did not complete the course or
* got disqualified.
*/
Result.prototype.completed = function () {
return this.totalTime !== null && !this.isDisqualified && !this.isOverMaxTime;
};
/**
* Returns whether the result has any times at all.
* @return {Boolean} True if the result includes at least one time,
* false if the result has no times.
*/
Result.prototype.hasAnyTimes = function () {
// Trim the leading zero
return this.originalCumTimes.slice(1).some(isNotNull);
};
/**
* Returns the split to the given control. If the control index given is zero
* (i.e. the start), zero is returned. If the competitor or team has no time
* recorded for that control, null is returned. If the value is missing,
* because the value read from the file was invalid, NaN is returned.
*
* @param {Number} controlIndex Index of the control (0 = start).
* @return {Number|null} The split time in seconds to the given control.
*/
Result.prototype.getSplitTimeTo = function (controlIndex) {
return (controlIndex === 0) ? 0 : this.splitTimes[controlIndex - 1];
};
/**
* Returns the 'original' split to the given control. This is always the
* value read from the source data file, or derived directly from this data,
* before any attempt was made to repair the data.
*
* If the control index given is zero (i.e. the start), zero is returned.
* If no time is recorded for that control, null is returned.
* @param {Number} controlIndex Index of the control (0 = start).
* @return {Number|null} The split time in seconds to the given control.
*/
Result.prototype.getOriginalSplitTimeTo = function (controlIndex) {
if (this.isNonStarter) {
return null;
} else {
return (controlIndex === 0) ? 0 : this.originalSplitTimes[controlIndex - 1];
}
};
/**
* Returns whether the control with the given index is deemed to have a
* dubious split time.
* @param {Number} controlIndex The index of the control.
* @return {Boolean} True if the split time to the given control is dubious,
* false if not.
*/
Result.prototype.isSplitTimeDubious = function (controlIndex) {
return (controlIndex > 0 && this.originalSplitTimes[controlIndex - 1] !== this.splitTimes[controlIndex - 1]);
};
/**
* Returns the cumulative split to the given control. If the control index
* given is zero (i.e. the start), zero is returned. If there is no
* cumulative time recorded for that control, null is returned. If no time
* is recorded, but the time was deemed to be invalid, NaN will be returned.
* @param {Number} controlIndex Index of the control (0 = start).
* @return {Number|null} The cumulative split time in seconds to the given control.
*/
Result.prototype.getCumulativeTimeTo = function (controlIndex) {
return this.cumTimes[controlIndex];
};
/**
* Returns the 'original' cumulative time the competitor or team took to the
* given control. This is always the value read from the source data file,
* before any attempt was made to repair the data.
* @param {Number} controlIndex Index of the control (0 = start).
* @return {Number|null} The cumulative split time in seconds to the given control.
*/
Result.prototype.getOriginalCumulativeTimeTo = function (controlIndex) {
return (this.isNonStarter) ? null : this.originalCumTimes[controlIndex];
};
/**
* Returns whether the control with the given index is deemed to have a
* dubious cumulative time.
* @param {Number} controlIndex The index of the control.
* @return {Boolean} True if the cumulative time to the given control is
* dubious, false if not.
*/
Result.prototype.isCumulativeTimeDubious = function (controlIndex) {
return this.originalCumTimes[controlIndex] !== this.cumTimes[controlIndex];
};
/**
* Returns the rank of the split to the given control. If the control index
* given is zero (i.e. the start), or if there is no time recorded for that
* control, or the ranks have not been set on this result, null is
* returned.
* @param {Number} controlIndex Index of the control (0 = start).
* @return {Number|null} The split rank to the given control.
*/
Result.prototype.getSplitRankTo = function (controlIndex) {
return (this.splitRanks === null) ? null : this.splitRanks[controlIndex];
};
/**
* Returns the rank of the cumulative split to the given control. If the
* control index given is zero (i.e. the start), or if there is no time
* recorded for that control, or if the ranks have not been set on this
* result, null is returned.
* @param {Number} controlIndex Index of the control (0 = start).
* @return {Number|null} The cumulative rank to the given control.
*/
Result.prototype.getCumulativeRankTo = function (controlIndex) {
return (this.cumRanks === null) ? null : this.cumRanks[controlIndex];
};
/**
* Returns the time loss at the given control, or null if time losses cannot
* be calculated or have not yet been calculated.
* @param {Number} controlIndex Index of the control.
* @return {Number|null} Time loss in seconds, or null.
*/
Result.prototype.getTimeLossAt = function (controlIndex) {
return (controlIndex === 0 || this.timeLosses === null) ? null : this.timeLosses[controlIndex - 1];
};
/**
* Returns all of the cumulative time splits.
* @return {Array} The cumulative split times in seconds for the competitor
* or team.
*/
Result.prototype.getAllCumulativeTimes = function () {
return this.cumTimes;
};
/**
* Returns all of the original cumulative time splits.
* @return {Array} The original cumulative split times in seconds for the
* competitor or team.
*/
Result.prototype.getAllOriginalCumulativeTimes = function () {
return this.originalCumTimes;
};
/**
* Returns all of the split times.
* @return {Array} The split times in seconds for the competitor or team.
*/
Result.prototype.getAllSplitTimes = function () {
return this.splitTimes;
};
/**
* Returns whether this result is missing a start time.
*
* The result is missing its start time if it doesn't have a start time and
* it also has at least one split. This second condition allows the Race
* Graph to be shown even if there are results with no times and no start
* time.
*
* @return {Boolean} True if there is no a start time, false if there is, or
* if they have no other splits.
*/
Result.prototype.lacksStartTime = function () {
return this.startTime === null && this.splitTimes.some(isNotNull);
};
/**
* Sets the split and cumulative-split ranks for this result. The first
* items in both arrays should be null, to indicate that the split and
* cumulative ranks don't make any sense at the start.
* @param {Array} splitRanks Array of split ranks for this result.
* @param {Array} cumRanks Array of cumulative-split ranks for this result.