eliste/dr_api.js

3253 lines
107 KiB
JavaScript

/**
* digital ROCK jQuery based Javascript API
*
* @link http://www.digitalrock.de
* @author Ralf Becker <RalfBecker@digitalROCK.de>
* @copyright 2010-18 by RalfBecker@digitalROCK.de
* @version $Id: dr_api.js 1250 2015-06-18 06:49:01Z ralfbecker $
*/
/**
* Widgets defined in this file:
*
* - DrWidget: universal widget, which can display all data-types and implement reload-free / in-place navigation
* - Resultlist: displays results and rankings, inherits from Startlist
* - Startlist: displays startlists and implementes automatic scrolling and rotation through multiple results
* - Results: displays top results of multiple categories and allows to change competition
* - Starters: displays registration data
* - Competitions: displays calendar, allows to change year and optional filter
* - Profile: display profile of an athlete based on a html template
* - ResultTemplate: displays result based on a html template
* - Aggregated: displays an aggregated ranking: national team ranking, GER sektionenwertung or SUI regionalzentren
* - DrBaseWidget: virtual base of all widgets implements json(p) loading of data
* - DrTable: creates and updates a table from data and a column definition, used by most widgets
*
* In almost all cases you only need to use DrWidget as shown in following example:
*
* <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
* <script type="text/javascript" src="http://www.digitalrock.de/egroupware/ranking/js/dr_api.js"></script>
* <link type="text/css" rel="StyleSheet" href="http://www.digitalrock.de/egroupware/ranking/templates/default/dr_list.css" />
*
* <div id="container" />
* <script>
* var widget;
* $(document).ready(function() {
* widget = new DrWidget('container', http://www.digitalrock.de/egroupware/ranking/json.php');
* }
* </script>
*
* @link http://www.digitalrock.de/egroupware/ranking/README describes available parameters for json url
* @link https://developers.google.com/webmasters/ajax-crawling/ describes supported ajax crawling scheme
* @link http://svn.outdoor-training.de/repos/trunk/ranking/inc/class.ranking_widget.inc.php php class implementing ajax crawling
*/
/**
* Example with multi-result scrolling (use c= and r= for cat and route)
*
* http://www.digitalrock.de/egroupware/ranking/sitemgr/digitalrock/eliste.html?comp=1251&cat=1&route=2&detail=0&rotate=c=1,r=2:c=2,r=2
*
* You can also supply an optional parameter w= (think of German "Wettkampf" as "c" was already taken) to rotate though different competitions (the first of which is specified by the "comp" parameter in the original URL.
*
* Example https://www.digitalrock.de/egroupware/ranking/sitemgr/digitalrock/eliste.html?comp=1395&beamer=1&cat=1&route=0&rotate=w=1395,c=1,r=0:w=1396,c=1,r=0
*
* The interesting part here is rotate=w=1395,c=1,r=0:w=1396,c=1,r=0
*/
/**
* Show nation specific footer
*
* @param {jQuery|String} selector where to show the footer
* @param {String} nation
*/
function showFooterByNation(selector, nation) {
var jelem = jQuery(selector);
var footer = "/icc_footer.inc.php";
switch (nation) {
case "GER":
footer = "/dav_footer.inc.php";
break;
case "SUI":
footer = "/sac_footer.inc.php p:not(.register)";
break;
}
jelem.load(footer, function () {
jelem.fadeIn("slow");
});
}
/**
* Show category specific footer
*
* @param {jQuery|String} selector where to show the footer
* @param {Number|String} cat default read it from cat parameter of document
*/
function showFooterByCat(selector, cat) {
if (!cat)
cat = (document.location.href.match(/cat=([a-z]{3}|\d+)/i) || [])[1];
var national_cats = {
GER: [
212, 211, 207, 294, 205, 14, 202, 201, 296, 200, 206, 213, 4, 215, 13,
108, 12, 11, 66, 7, 226, 223, 217, 216, 214, 25, 54, 109, 107, 106, 67,
48, 49, 50, 51, 52, 53, 110, 111, 113, 114, 28, 27, 26, 112, 115,
],
SUI: [
47, 46, 44, 116, 30, 35, 69, 68, 43, 41, 209, 208, 219, 222, 31, 32, 33,
34, 36, 37, 38, 39,
],
};
for (var nation in national_cats) {
if (
cat.toUpperCase() === nation ||
national_cats[nation].indexOf(parseInt(cat)) >= 0
) {
showFooterByNation(selector, nation);
return;
}
}
showFooterByNation(selector);
}
/**
* Baseclass for all widgets
*
* We only use jQuery() here (not $() or $j()!) to be able to run as well inside EGroupware as with stock jQuery from googleapis.
*/
var DrBaseWidget = (function () {
/**
* Constructor for all widgets from given json url
*
* Table get appended to specified _container
*
* @param _container
* @param _json_url url for data to load
*/
function DrBaseWidget(_container, _json_url) {
this.json_url = _json_url;
this.container = jQuery(
typeof _container == "string" ? "#" + _container : _container
);
this.container.addClass(this.constructor.name);
}
/**
* Install update method as popstate or hashchange handler
*/
DrBaseWidget.prototype.installPopState = function () {
// add popstate or hashchange (IE8,9) event listener, to use browser back button for navigation
// some browsers, eg. Chrome, generate a pop on inital page-load
// to prevent loading page initially twice, we store initial location
this.prevent_initial_pop = location.href;
var initial_params = this.json_url.replace(/^.*(#!|#|\?)/, "") || "";
var that = this;
jQuery(window).bind(
window.history.pushState ? "popstate" : "hashchange",
function (e) {
if (
!that.prevent_initial_pop ||
that.prevent_initial_pop != location.href
) {
that.update(location.hash || location.query || initial_params);
}
delete that.prevent_initial_pop;
}
);
};
/**
* Update Widget from json_url
*
* To be able to cache jsonp requests in CDN, we have to use the same callback.
* Using same callback leads to problems with concurrent requests: failed: parsererror (jsonp was not called)
* To work around that we queue jsonp request, if there's already one running.
*
* Queue is maintained globally in DrBaseWidget.jsonp_queue, as requests come from different objects!
*
* @param {boolean} ignore_queue used internally to start next object in queue without requeing it
*/
DrBaseWidget.prototype.update = function (ignore_queue) {
// remove our own parameters and current year from json url to improve caching
var url = this.json_url
.replace(/(detail|beamer|rotate|toc)=[^&]*(&|$)/, "")
.replace(new RegExp("year=" + new Date().getFullYear() + "(&|$)"), "")
.replace(/&$/, "");
// do we need a jsonp request
var jsonp =
this.json_url.indexOf("//") != -1 &&
this.json_url.split("/", 3) != location.href.split("/", 3);
if (typeof DrBaseWidget.jsonp_queue == "undefined")
DrBaseWidget.jsonp_queue = [];
if (!ignore_queue && jsonp) {
// add us to the queue
DrBaseWidget.jsonp_queue.push(this);
}
// if there's only one in the queue (or no queueing necessary: no jsonp) --> send ajax request
if (ignore_queue || !jsonp || DrBaseWidget.jsonp_queue.length == 1) {
jQuery.ajax({
url: url,
async: true,
context: this,
data: "",
dataType: jsonp ? "jsonp" : "json",
jsonpCallback: "jsonp", // otherwise jQuery generates a random name, not cachable by CDN
cache: true,
type: "GET",
success: function (_data) {
// if we are first object in queue, remove us
if (DrBaseWidget.jsonp_queue[0] === this)
DrBaseWidget.jsonp_queue.shift();
// if someone left in queue, run it's update ignore the queue
if (DrBaseWidget.jsonp_queue.length)
DrBaseWidget.jsonp_queue[0].update(true);
this.handleResponse(_data);
},
error: function (_xmlhttp, _err, _status) {
// need same handling as success
if (DrBaseWidget.jsonp_queue[0] === this)
DrBaseWidget.jsonp_queue.shift();
if (DrBaseWidget.jsonp_queue.length)
DrBaseWidget.jsonp_queue[0].update(true);
//if (_err != 'timeout') alert('Ajax request to '+this.json_url+' failed: '+_err+(_status?' ('+_status+')':''));
// schedule update again after 60sec
this.update_handle = window.setTimeout(
jQuery.proxy(this.update, this, ignore_queue),
60000
);
},
});
}
};
/**
* Callback for loading data via ajax
*
* Virtual, need to be implemented in inheriting objects!
*
* @param _data
*/
DrBaseWidget.prototype.handleResponse = function (_data) {
throw (
"No handleResponse implemented in " +
this.constructor.name +
" inheriting from DrBaseWidget!"
);
};
/**
* Add list with see also links, if not beamer or toc disabled
*
* @param _see_also
*/
DrBaseWidget.prototype.seeAlso = function (_see_also) {
this.container.find("ul.seeAlso").remove();
if (
typeof _see_also != "undefined" &&
_see_also.length > 0 &&
!this.json_url.match(/toc=0/) &&
!this.json_url.match(/beamer=1/)
) {
var ul = jQuery(document.createElement("ul")).attr("class", "seeAlso");
ul.prepend(document.createTextNode(this.lang("See also:")));
for (var i = 0; i < _see_also.length; ++i) {
var tag = jQuery(document.createElement("li"));
ul.append(tag);
if (_see_also[i].url) {
var a = jQuery(document.createElement("a")).attr(
"href",
_see_also[i].url
);
tag.append(a);
if (this.navigateTo) {
a.click(this.navigateTo);
}
tag = a;
}
tag.text(_see_also[i].name);
}
this.container.append(ul);
}
};
/**
* Replace attribute named from with one name to and value keeping the order of the attributes
*
* @param {object} obj
* @param {string} from
* @param {string} to
* @param {*} value
*/
DrBaseWidget.prototype.replace_attribute = function (obj, from, to, value) {
var found = false;
for (var attr in obj) {
if (!found) {
if (attr == from) {
found = true;
delete obj[attr];
obj[to] = value;
}
} else {
var val = obj[attr];
delete obj[attr];
obj[attr] = val;
}
}
};
/**
* Replace "nation" column with what's specified in "display_athlete" on competition
*
* @param {string} _display_athlete
* @param {string} _nation
*/
DrBaseWidget.prototype.replace_nation = function (_display_athlete, _nation) {
switch (_display_athlete) {
case "none":
delete this.columns.nation;
break;
case "city":
case "pc_city":
this.replace_attribute(this.columns, "nation", "city", "City");
break;
case "federation":
case "fed_and_parent":
var fed_label = "Federation";
switch (_nation) {
case "GER":
fed_label = "DAV Sektion";
break;
case "SUI":
fed_label = "Sektion";
break;
}
this.replace_attribute(this.columns, "nation", "federation", fed_label);
break;
}
};
/**
* Format a date according to browser local format
*
* @param {string} _ymd yyyy-mm-dd string or everything understood by date constructor
* @returns {string}
*/
DrBaseWidget.prototype.formatDate = function (_ymd) {
if (!_ymd || typeof _ymd != "string") return "";
var date = new Date(_ymd);
return date.toLocaleDateString().replace(/[0-9]+\./g, function (_match) {
return _match.length <= 2 ? "0" + _match : _match;
});
};
/**
* Translate english phrase into user language
*
* @param {string} _msg
* @returns {string}
*/
DrBaseWidget.prototype.lang = function (_msg) {
if (_msg === null) {
return "";
}
if (typeof _msg !== "string" && _msg) {
console.log("Cannot translate an object", _msg);
return _msg;
}
var translation = _msg;
if (typeof dr_translations !== "undefined") {
if (typeof DrBaseWidget.user_lang === "undefined") {
var language =
(navigator.languages && navigator.languages[0]) || // Chrome / Firefox
navigator.language || // All browsers
navigator.userLanguage; // IE <= 10
DrBaseWidget.user_lang = language.replace(/-[A-Z]+/, "");
}
if (
dr_translations[_msg] &&
dr_translations[_msg][DrBaseWidget.user_lang]
) {
translation = dr_translations[_msg][DrBaseWidget.user_lang];
}
}
if (arguments.length == 1) return translation;
if (arguments.length == 2) return translation.replace("%1", arguments[1]);
// to cope with arguments containing '%2' (eg. an urlencoded path like a referer),
// we first replace all placeholders '%N' with '|%N|' and then we replace all '|%N|' with arguments[N]
translation = translation.replace(/%([0-9]+)/g, "|%$1|");
for (var i = 1; i < arguments.length; ++i) {
translation = translation.replace("|%" + i + "|", arguments[i]);
}
return translation;
};
return DrBaseWidget;
})();
/**
* DrTable helper to construct table from colum-defition and data
*/
var DrTable = (function () {
/**
* Constructor for table with given data and columns
*
* Table get appended to specified _container
*
* @param _data array with data for each participant
* @param _columns hash with column name => header
* @param _sort column name to sort by
* @param _ascending
* @param _quota quota if quota line should be drawn in result
* @param _navigateTo click method for profiles
* @param _showUnranked if true AND _sort=='result_rank' show participants without rank, default do NOT show them
*/
function DrTable(
_data,
_columns,
_sort,
_ascending,
_quota,
_navigateTo,
_showUnranked
) {
this.data = _data;
this.columns = _columns;
if (typeof _sort == "undefined") for (_sort in _columns) break;
this.sort = _sort;
if (typeof _ascending == "undefined") _ascending = true;
this.ascending = _ascending;
this.quota = parseInt(_quota);
this.navigateTo = _navigateTo;
this.showUnranked = _showUnranked ? true : false;
// hash with PerId => tr containing athlete
this.athletes = {};
this.sortData();
// header
this.dom = document.createElement("table");
jQuery(this.dom).addClass("DrTable");
var thead = document.createElement("thead");
jQuery(this.dom).append(thead);
var row = this.createRow(this.columns, "th");
jQuery(thead).append(row);
// athletes
var tbody = jQuery(document.createElement("tbody"));
jQuery(this.dom).append(tbody);
for (var i = 0; i < this.data.length; ++i) {
var data = this.data[i];
if (Array.isArray(data.results)) {
// category with result
if (typeof this.column_count == "undefined") {
this.column_count = 0;
for (var c in this.columns) ++this.column_count;
}
if (typeof data.name != "undefined") {
row = document.createElement("tr");
var th = jQuery(document.createElement("th"));
th.attr("colspan", this.column_count);
th.text(data.name);
if (typeof data.url != "undefined") {
var a = jQuery(document.createElement("a"));
a.attr("href", data.url);
a.text(this.lang("Complete Results"));
if (this.navigateTo || typeof data.click != "undefined")
a.click(this.navigateTo || data.click);
th.append(a);
}
jQuery(row).append(th);
tbody.append(row);
}
for (var j = 0; j < data.results.length; ++j) {
tbody.append(this.createRow(data.results[j]));
}
} // single result row
else {
if (
this.sort == "result_rank" &&
((typeof data.result_rank == "undefined" && !this.showUnranked) ||
data.result_rank < 1)
) {
break; // no more ranked competitiors
}
tbody.append(this.createRow(data));
}
}
//console.log(this.athletes);
}
DrTable.prototype.lang = DrBaseWidget.prototype.lang;
/**
* Update table with new data, trying to re-use existing rows
*
* @param _data array with data for each participant
* @param _quota quota if quota line should be drawn in result
*/
DrTable.prototype.update = function (_data, _quota) {
this.data = _data;
if (typeof _quota != "undefined") this.quota = parseInt(_quota);
//console.log(this.data);
this.sortData();
var tbody = this.dom.firstChild.nextSibling;
var pos;
// uncomment to test update: reverses the list on every call
//if (this.data[0].PerId == tbody.firstChild.id) this.data.reverse();
var athletes = this.athletes;
this.athletes = {};
for (var i = 0; i < this.data.length; ++i) {
var data = this.data[i];
var row;
if (data.PerId != "undefined") {
row = athletes[data.PerId];
} else if (data.team_id != "undefined") {
row = athletes[data.team_id];
}
if (
this.sort == "result_rank" &&
(typeof data.result_rank == "undefined" || data.result_rank < 1)
) {
break; // no more ranked competitiors
}
// search athlete in tbody
if (typeof row != "undefined") {
//jQuery(row).detach();
//this.updateRow(row,data);
jQuery(row).remove();
}
//else
{
row = this.createRow(data);
}
// no child in tbody --> append row
if (typeof pos == "undefined") {
jQuery(tbody).prepend(row);
} else {
jQuery(pos).after(row);
}
pos = row;
}
// remove further rows / athletes not in this.data
if (typeof pos != "undefined" && typeof pos.nextSibling != "undefined") {
jQuery("#" + pos.id + " ~ tr").remove();
}
};
/**
* Update given data-row with changed content
*
* @param {object} _row
* @param {object} _data
* @todo
*/
DrTable.prototype.updateRow = function (_row, _data) {};
/**
* Create new data-row with all columns from this.columns
*
* @param {object} _data
* @param {string} [_tag=td]
*/
DrTable.prototype.createRow = function (_data, _tag) {
//console.log(_data);
if (typeof _tag == "undefined") _tag = "td";
var row = document.createElement("tr");
if (typeof _data.PerId != "undefined" && _data.PerId > 0) {
row.id = _data.PerId;
if (_data.className) {
row.className = _data.className;
if (row.className.match(/hideNames/))
row.title = this.lang("Athlete asked not to show his name anymore.");
}
this.athletes[_data.PerId] = row;
} else if (typeof _data.team_id != "undefined" && _data.team_id > 0) {
row.id = _data.team_id;
this.athletes[_data.team_id] = row;
}
var span = 1;
for (var col in this.columns) {
if (--span > 0) continue;
var url = _data.url;
// if object has a special getter func, call it
var col_data;
if (typeof this.columns[col] == "function") {
col_data = this.columns[col].call(this, _data, _tag, col);
} else {
col_data = _data[col];
}
// allow /-delemited expressions to index into arrays and objects
if (typeof col_data == "undefined" && col.indexOf("/") != -1) {
var parts = col.split("/");
col_data = _data;
for (var p in parts) {
col = parts[p];
if (col == "lastname" || col == "firstname") url = col_data.url;
if (typeof col_data != "undefined") col_data = col_data[col];
}
} else if (col.indexOf("/") != -1)
col = col.substr(col.lastIndexOf("/") + 1);
var tag = document.createElement(_tag);
tag.className = col;
jQuery(row).append(tag);
// add pstambl link to name & vorname
if (
typeof url != "undefined" &&
(col == "lastname" || col == "firstname")
) {
var a = document.createElement("a");
a.href = url;
a.target = "pstambl";
if (this.navigateTo && url.indexOf("#") != -1)
jQuery(a).click(this.navigateTo);
jQuery(tag).append(a);
tag = a;
}
if (
typeof _data.fed_url != "undefined" &&
(col == "nation" || col == "federation")
) {
var a = document.createElement("a");
a.href = _data.fed_url;
a.target = "_blank";
jQuery(tag).append(a);
tag = a;
}
if (typeof col_data == "object" && col_data) {
if (typeof col_data.nodeName != "undefined") {
jQuery(tag).append(col_data);
} else {
if (col_data.colspan > 1) tag.colSpan = span = col_data.colspan;
if (col_data.className) tag.className = col_data.className;
if (col_data.title) tag.title = col_data.title;
if (col_data.url || col_data.click) {
var a = document.createElement("a");
a.href = col_data.url || "#";
if (col_data.click || this.navigateTo) {
jQuery(a).click(col_data.click || this.navigateTo);
}
jQuery(tag).append(a);
tag = a;
}
if (col_data.nodes) {
jQuery(tag).append(col_data.nodes);
} else {
jQuery(tag).text(col_data.label);
}
}
} else {
jQuery(tag).text(typeof col_data != "undefined" ? col_data : "");
span = 1;
}
}
// add or remove quota line
if (
this.sort == "result_rank" &&
this.quota &&
_data.result_rank &&
parseInt(_data.result_rank) >= 1 &&
parseInt(_data.result_rank) > this.quota
) {
row.className = "quota_line";
delete this.quota; // to set quota line only once
}
return row;
};
/**
* Sort data according to sort criteria
*
* @todo get using this.sortNummeric callback working
*/
DrTable.prototype.sortData = function () {
function sortResultRank(_a, _b) {
var rank_a = _a["result_rank"];
if (typeof rank_a == "undefined" || rank_a < 1) rank_a = 9999;
var rank_b = _b["result_rank"];
if (typeof rank_b == "undefined" || rank_b < 1) rank_b = 9999;
var ret = rank_a - rank_b;
if (!ret) ret = _a["lastname"] > _b["lastname"] ? 1 : -1;
if (!ret) ret = _a["firstname"] > _b["firstname"] ? 1 : -1;
return ret;
}
switch (this.sort) {
case false: // dont sort
break;
case "result_rank":
// not necessary as server returns them sorted this way
//this.data.sort(sortResultRank);
break;
default:
var sort = this.sort;
this.data.sort(function (_a, _b) {
var a = sort == "start_order" ? parseInt(_a[sort]) : _a[sort];
var b = sort == "start_order" ? parseInt(_b[sort]) : _b[sort];
return a == b ? 0 : a < b ? -1 : 1;
//return _a[sort] == _b[sort] ? 0 : (_a[sort] < _b[sort] ? -1 : 1);
});
break;
}
if (!this.ascending) this.data.reverse();
};
return DrTable;
})();
/**
* Startlist widget inheriting from DrBaseWidget
*/
var Startlist = (function () {
/**
* Constructor for startlist from given json url
*
* Table get appended to specified _container
*
* @param _container
* @param _json_url url for data to load
* @param {boolean} _no_navigation do NOT display TOC
*/
function Startlist(_container, _json_url, _no_navigation) {
DrBaseWidget.prototype.constructor.call(this, _container, _json_url);
this.no_navigation = _no_navigation;
// do not continue, as constructor is called when inheriting from Startlist without parameters!
if (typeof _container == "undefined") return;
// Variables needed for scrolling in upDown
// scroll speed
this.scroll_by = 1;
// scroll interval, miliseconds (will be changed on resized windows where scroll_by is increased, so a constant scrolling speed is maintained)
this.scroll_interval = 20;
// current scrolling direction. 1: down, -1: up
this.scroll_dir = 1;
// sleep on the borders for sleep_for seconds
this.sleep_for = 4;
// margin in which to reverse scrolling
// CAUTION: At the beginning, we scroll pixelwise through the margin, one pixel each sleep_for seconds. Do not change the margin unless you know what you do.
this.margin = 2;
// helper variable
var now = new Date();
this.sleep_until = now.getTime() + 10000;
this.first_run = true;
this.do_rotate = false;
this.update();
if (this.json_url.match(/rotate=/)) {
var list = this;
// 20110716: This doesn't seem to be needed anymore. Comment it for now.
//window.scrollBy(0, 20);
this.scroll_interval_handle = window.setInterval(function () {
list.upDown();
}, 20);
}
}
// inherit from DrBaseWidget
Startlist.prototype = new DrBaseWidget();
Startlist.prototype.constructor = Startlist;
/**
* Callback for loading data via ajax
*
* @param _data route data object
*/
Startlist.prototype.handleResponse = function (_data) {
//console.log(_data);
var detail = this.json_url.match(/detail=([^&]+)/);
if (detail) detail = detail[1];
switch (_data.discipline) {
case "speedrelay":
this.startlist_cols =
detail === null
? {
// default detail
start_order: this.lang("StartNr"),
team_name: this.lang("Teamname"),
"athletes/0/lastname": this.lang("Athlete #1"),
"athletes/1/lastname": this.lang("Athlete #2"),
"athletes/2/lastname": this.lang("Athlete #3"),
}
: detail
? {
// detail=1
start_order: this.lang("StartNr"),
team_name: this.lang("Teamname"),
//'team_nation': 'Nation',
"athletes/0/lastname": {
label: this.lang("Athlete #1"),
colspan: 3,
},
"athletes/0/firstname": "",
"athletes/0/result_time": "",
"athletes/1/lastname": {
label: this.lang("Athlete #2"),
colspan: 3,
},
"athletes/1/firstname": "",
"athletes/1/result_time": "",
"athletes/2/lastname": {
label: this.lang("Athlete #3"),
colspan: 3,
},
"athletes/2/firstname": "",
"athletes/2/result_time": "",
}
: {
// detail=0
start_order: this.lang("StartNr"),
team_name: this.lang("Teamname"),
team_nation: this.lang("Nation"),
};
break;
case "combined":
this.result_cols.final_points = this.lang("Final Points");
delete this.result_cols.start_number; // table is far too big anyway
// fall through
default:
this.startlist_cols = {
start_order: { label: this.lang("StartNr"), colspan: 2 },
start_number: "",
lastname: { label: this.lang("Name"), colspan: 2 },
firstname: "",
birthyear: this.lang("Birthyear"),
nation: this.lang("Nation"),
};
break;
}
// if quali_preselected and heat = 1||2, we have to use a function to get either start_order or text "preselected"
if (
_data.quali_preselected &&
(_data.route_order == 0 || _data.route_order == 1)
) {
var quali_preselected = _data.quali_preselected;
var start_order = this.startlist_cols.start_order;
this.startlist_cols.start_order = function (_data, _tag, col) {
if (_tag == "th") return start_order;
if (_data.ranking <= quali_preselected)
return this.lang("Vorqualifiziert"); //'preselected';
return _data[col];
};
}
var sort;
// if we have no result columns or no ranked participant, show a startlist
if (
typeof this.result_cols == "undefined" ||
(_data.participants[0] &&
!_data.participants[0].result_rank &&
_data.discipline != "ranking")
) {
this.columns = this.startlist_cols;
sort = "start_order";
this.container.attr("class", "Startlist");
}
// if we are a result showing a startlist AND have now a ranked participant
// --> switch back to result
else {
this.columns = this.result_cols;
sort = "result_rank";
}
this.replace_nation(_data.display_athlete, _data.nation);
// fix route_names containing only one or two qualifications are send as array because index 0 and 1
if (Array.isArray(_data.route_names)) {
var route_names = _data.route_names;
delete _data.route_names;
_data.route_names = {};
for (var i = 0; i < route_names.length; ++i) {
_data.route_names[i] = route_names[i];
}
}
// keep route_names to detect additional routes on updates
if (typeof this.route_names == "undefined") {
this.route_names = _data.route_names;
}
// remove whole table, if the discipline is speed and the number of route_names changes
if (_data.discipline == "speed" && this.json_url.match(/route=-1/)) {
// && this.route_names != _data.route_names)
for (var i = 2; i < 10; i++) {
if (typeof _data.route_names[i] != typeof this.route_names[i]) {
// there was an update of the route_names array
this.route_names = _data.route_names;
jQuery(this.container).empty();
delete this.table;
break;
}
}
}
// remove whole table, if discipline or startlist/resultlist (detemined by sort) changed
if (
(this.discipline && this.discipline != _data.discipline) ||
(this.sort && this.sort != sort) ||
_data.route_order != this.route_order || // switching heats (they can have different columns)
detail !== this.detail || // switching detail on/off
this.json_url != this.last_json_url
) {
jQuery(this.container).empty();
delete this.table;
}
this.discipline = _data.discipline;
this.sort = sort;
this.route_order = _data.route_order;
this.detail = detail;
this.last_json_url = this.json_url;
if (typeof this.table == "undefined") {
// for general result use one column per heat
if (this.columns.result && _data.route_names && _data.route_order == -1) {
delete this.columns.result;
// show final first and 2. quali behind 1. quali: eg. 3, 2, 0, 1
var routes = [];
if (_data.route_names["1"]) routes.push("1");
for (var id in _data.route_names) {
if (id != "-1" && id != "1") routes.push(id);
}
routes.reverse();
for (var i = 0; i < routes.length; ++i) {
var route = routes[i];
// for ranking, we add link to results
if (_data.discipline == "ranking") {
var id = route.replace(/ $/, ""); // remove space append to force js to keep the order
var comp_cat = id.split("_");
this.columns["result" + id] = {
label: _data.route_names[route],
url:
"#!comp=" +
comp_cat[0] +
"&cat=" +
(comp_cat[1] || _data.cat.GrpId),
};
} else {
this.columns["result" + route] = _data.route_names[route];
}
}
// evtl. add points column
if (_data.participants[0] && _data.participants[0].quali_points) {
this.columns["quali_points"] = this.lang("Points");
// delete single qualification results
if (this.no_navigation) {
delete this.columns.result0;
delete this.columns.result1;
if (_data.discipline == "combined") delete this.columns.result2;
this.columns["quali_points"] =
_data.discipline == "combined"
? this.lang("Quali.")
: this.lang("Qualification");
}
}
if (
_data.discipline == "combined" &&
_data.participants[0] &&
typeof _data.participants[0].final_points == "undefined"
) {
delete this.columns.final_points;
}
title_prefix = "";
}
if (
this.columns.result &&
_data.participants[0] &&
_data.participants[0].rank_prev_heat &&
!this.json_url.match(/detail=0/)
) {
this.columns["rank_prev_heat"] = this.lang("previous heat");
}
// competition
this.comp_header = jQuery(document.createElement("h1"));
jQuery(this.container).append(this.comp_header);
this.comp_header.addClass("compHeader");
// result date
this.result_date = jQuery(document.createElement("h3"));
jQuery(this.container).append(this.result_date);
this.result_date.addClass("resultDate");
// route header
this.header = jQuery(document.createElement("h1"));
jQuery(this.container).append(this.header);
this.header.addClass("listHeader");
// display a toc with all available heats, if not explicitly disabled (toc=0) or beamer
this.displayToc(_data);
// create new table
if (!_data.error && _data.participants.length) {
this.table = new DrTable(
_data.participants,
this.columns,
this.sort,
true,
_data.route_result ? _data.route_quota : null,
this.navigateTo,
_data.discipline == "ranking" &&
(detail || !_data.participants[0].result_rank)
);
if (_data.participants[0].result_rank)
jQuery(this.table.dom).addClass(_data.discipline);
jQuery(this.container).append(this.table.dom);
}
this.seeAlso(_data.see_also);
} else {
// update a toc with all available heats, if not explicitly disabled (toc=0) or beamer
this.displayToc(_data);
// update existing table
this.table.update(
_data.participants,
_data.route_result ? _data.route_quota : null
);
}
// set/update header line
this.setHeader(_data);
// if route is NOT offical, update list every 10 sec, of category not offical update every 5min (to get new heats)
if (!_data.category_offical && this.discipline != "ranking") {
var list = this;
this.update_handle = window.setTimeout(function () {
list.update();
}, _data.expires * 1000);
//console.log('setting up refresh in '+_data.expires+' seconds');
}
};
/**
* Create or update TOC (list of available routes for navigation)
*
* Can be disabled via "toc=0" or "beamer=1" in json_url. Always disabled for rankings.
*
* @param _data route data object
*/
Startlist.prototype.displayToc = function (_data) {
if (
this.json_url.match(/toc=0/) ||
this.json_url.match(/beamer=1/) ||
this.discipline == "ranking" ||
this.no_navigation
) {
return; // --> no toc
}
var toc = this.container.find("ul.listToc");
var new_toc = !toc.length;
if (new_toc) toc = jQuery(document.createElement("ul")).addClass("listToc");
else toc.empty();
var href = location.href.replace(/\?(.*)#/, "#"); // prevent query and hash messing up navigation
for (var r in _data.route_names) {
if (r != this.route_order) {
var li = jQuery(document.createElement("li"));
var a = jQuery(document.createElement("a"));
a.text(_data.route_names[r].replace(" - ", "-"));
var reg_exp = /route=[^&]+/;
var url = href.replace(reg_exp, "route=" + r);
if (url.indexOf("route=") == -1) url += "&route=" + r;
a.attr("href", url);
if (this.navigateTo) {
a.click(this.navigateTo);
} else {
var that = this;
a.click(function (e) {
that.json_url = that.json_url.replace(
reg_exp,
this.href.match(reg_exp)[0]
);
if (that.json_url.indexOf("route=") == -1)
that.json_url += "&route=" + r;
that.update();
e.preventDefault();
});
}
li.append(a);
toc.prepend(li);
}
}
// only add toc, if we have more then one route
if (!new_toc) {
// already added
} else if (toc.children().length) {
jQuery(this.container).append(toc);
} else {
toc.remove();
}
// add category toc
if (typeof _data.categorys == "undefined") return;
var toc = this.container.find("ul.listCatToc");
var new_toc = !toc.length;
if (new_toc)
toc = jQuery(document.createElement("ul")).addClass("listCatToc");
else toc.empty();
var cats = this.shortenNames(_data.categorys, "name");
for (var i = 0; i < cats.length; ++i) {
var cat = cats[i];
if (cat.GrpId != _data.GrpId) {
var li = jQuery(document.createElement("li"));
var a = jQuery(document.createElement("a"));
a.text(cat.name);
var reg_exp = /cat=[^&]+/;
var url = href.replace(reg_exp, "cat=" + cat.GrpId);
if (url.indexOf("cat=") == -1) url += "&cat=" + cat.GrpId;
a.attr("href", url);
if (this.navigateTo) {
a.click(this.navigateTo);
} else {
var that = this;
a.click(function (e) {
that.json_url = that.json_url.replace(
reg_exp,
this.href.match(reg_exp)[0]
);
if (that.json_url.indexOf("cat=") == -1)
that.json_url += "cat=" + cat.GrpId;
that.update();
e.preventDefault();
});
}
li.append(a);
toc.append(li);
}
}
// only add toc, if we have more then one route
if (!new_toc) {
// already added
} else if (toc.children().length) {
jQuery(this.container).append(toc);
} else {
toc.remove();
}
};
/**
* Shorten several names by removing parts common to all and remove spacing (eg. "W O M E N" --> "WOMEN")
*
* shortenNames["M E N speed", "W O M E N speed"]) returns ["MEN", "WOMEN"]
*
* @param {array} names array of strings or objects with attribute attr
* @param {string} attr attribute name to use or undefined
* @return {array}
*/
Startlist.prototype.shortenNames = function (names, attr) {
if (!jQuery.isArray(names) || !names.length) return names;
var split_by_regexp = / +/;
var spacing_regexp = /([A-Z]) ([A-Z])/;
var strs = [];
for (var i = 0; i < names.length; ++i) {
var name = names[i];
if (attr) name = name[attr];
do {
var n = name;
name = name.replace(spacing_regexp, "$1$2");
} while (n != name);
strs.push(name.split(split_by_regexp));
}
var first = [].concat(strs[0]);
for (var i = 0; i < first.length; ++i) {
for (var j = 1; j < strs.length; ++j) {
if (jQuery.inArray(first[i], strs[j]) == -1) {
break;
}
}
if (j == strs.length) {
// in all strings --> remove first[i] from all strings
for (var j = 0; j < strs.length; ++j) {
strs[j].splice(jQuery.inArray(first[i], strs[j]), 1);
}
}
}
for (var j = 0; j < strs.length; ++j) {
strs[j] = strs[j].join(" ");
if (attr) {
names[j][attr] = strs[j];
} else {
names[j] = strs[j];
}
}
return names;
};
/**
* Set header with a (provisional) Result or Startlist prefix
*
* @param _data
* @return
*/
Startlist.prototype.setHeader = function (_data) {
var title_prefix =
(this.sort == "start_order"
? this.lang("Startlist")
: _data.route_result
? this.lang("Result")
: this.lang("provisional Result")) + ": ";
var header = _data.route_name;
// if NOT detail=0 and not for general result, add prefix before route name
if (!this.json_url.match(/detail=0/) && _data.route_order != -1)
header = title_prefix + header;
document.title = header;
this.comp_header.empty();
this.comp_header.text(_data.comp_name);
this.result_date.empty();
if (_data.error) {
this.result_date.text(_data.error);
this.result_date.removeClass("resultDate");
this.result_date.addClass("error");
} else if (_data.route_result) {
this.result_date.text(_data.route_result);
this.result_date.prepend(
document.createTextNode(this.lang("As of") + " ")
);
this.result_date.append(
document.createTextNode(" " + this.lang("after"))
);
}
this.header.empty();
this.header.text(header);
};
/**
* Return the current scrolling position, which is the top of the current view.
*/
Startlist.prototype.currentTopPosition = function () {
var y = 0;
if (window.pageYOffset) {
// all other browsers
y = window.pageYOffset;
} else if (document.body && document.body.scrollTop) {
// IE
y = document.body.scrollTop;
}
return y;
};
Startlist.prototype.upDown = function () {
// check whether to sleep
var now = new Date();
var now_ms = now.getTime();
if (now_ms < this.sleep_until) {
// sleep: in this case we do nothing
return;
}
if (this.do_rotate) {
// we scheduled a rotation. Do it and then return.
this.rotateURL();
// wait for the page to build
this.sleep_until = now.getTime() + 1000;
this.first_run = true;
this.do_rotate = false;
// reset scroll_by and scroll_interval, which might have been changed when the windows was resized.
this.scroll_by = 1;
this.scroll_interval = 20;
//console.log("reset scroll_by to " + this.scroll_by);
return;
}
// Get current position
var y = 0;
var viewHeight = window.innerHeight;
var pageHeight = document.body.offsetHeight;
y = this.currentTopPosition();
// Do the scrolling
window.scrollBy(0, this.scroll_by * this.scroll_dir);
// Check, if scrolling worked
var new_y = 0;
new_y = this.currentTopPosition();
if (y == new_y) {
this.scroll_by += 1;
//console.log("increased scroll_by to " + this.scroll_by);
// reconfigure the scroll interval to maintain a constant speed
this.scroll_interval *= this.scroll_by;
this.scroll_interval /= this.scroll_by - 1;
//console.log("scroll_interval is now " + this.scroll_interval + " ms");
window.clearInterval(this.scroll_interval_handle);
var list = this;
this.scroll_interval_handle = window.setInterval(function () {
list.upDown();
}, this.scroll_interval);
}
// Set scrolling and sleeping parameters accordingly
var scrollTopPosition = y;
var scrollBottomPosition = y + viewHeight;
//alert("pageYOffset(y)="+pageYOffset+", innerHeight(wy)="+innerHeight+", offsetHeight(dy)="+document.body.offsetHeight);
var do_sleep = 0;
if (pageHeight <= viewHeight) {
// No scrolling at all
//console.log("Showing whole page");
do_sleep = 2;
this.do_rotate = true;
} else if (
this.scroll_dir != -1 &&
pageHeight - scrollBottomPosition <= this.margin
) {
// UP
this.scroll_dir = -1;
this.first_run = false;
do_sleep = 1;
} else if (this.scroll_dir != 1 && scrollTopPosition <= this.margin) {
// DOWN
this.scroll_dir = 1;
if (!this.first_run) {
do_sleep = 1;
this.do_rotate = true;
}
}
// Arm the sleep timer
//if (do_sleep > 0) { console.log("Sleeping for " + do_sleep * this.sleep_for + " seconds"); }
this.sleep_until = now.getTime() + this.sleep_for * 1000 * do_sleep;
};
Startlist.prototype.rotateURL = function () {
var rotate_url_matches = this.json_url.match(/rotate=([^&]+)/);
if (rotate_url_matches) {
var urls = rotate_url_matches[1];
//console.log(urls);
var current_comp = this.json_url.match(/comp=([^&]+)/)[1];
var current_cat = this.json_url.match(/cat=([^&]+)/)[1];
var current_route = this.json_url.match(/route=([^&]+)/)[1];
//console.log(current_cat);
var next = urls.match(
"(?:^|:|w=" +
current_comp +
",)" +
"c=" +
current_cat +
",r=" +
current_route +
":(?:w=([0-9_a-z]+),)?" +
"c=([0-9_a-z]+),r=(-?[\\d]+)"
);
//console.log(next);
if (!next) {
// at the end of the list, take the first argument
next = urls.match(
"^(?:w=([0-9_a-z]+),)?" + "c=([0-9_a-z]+),r=(-?[\\d]+)"
);
//console.log("starting over");
//console.log(next);
}
// We might not find a next competition in the rotate parameter
var next_comp = current_comp;
if (next[1]) {
next_comp = next[1];
}
// Extract category and route
var next_cat = next[2];
var next_route = next[3];
//console.log("current_cat = " + current_cat + ", current_route = " + current_route + ", next_cat = " + next_cat + ", next_route = " + next_route);
this.json_url = this.json_url.replace(
/comp=[0-9_a-z]+/,
"comp=" + next_comp
);
this.json_url = this.json_url.replace(
/cat=[0-9_a-z]+/,
"cat=" + next_cat
);
this.json_url = this.json_url.replace(
/route=[\d]+/,
"route=" + next_route
);
//console.log(this.json_url);
// cancel the currently pending request before starting a new one.
window.clearTimeout(this.update_handle);
this.update();
}
};
return Startlist;
})();
/**
* Resultlist widget inheriting from Startlist
*/
var Resultlist = (function () {
/**
* Constructor for result from given json url
*
* Table get appended to specified _container
*
* @param _container
* @param _json_url url for data to load
* @param {boolean} _no_navigation
*/
function Resultlist(_container, _json_url, _no_navigation) {
Startlist.prototype.constructor.call(
this,
_container,
_json_url,
_no_navigation
);
}
// inherit from Startlist
Resultlist.prototype = new Startlist();
Resultlist.prototype.constructor = Resultlist;
/**
* Callback for loading data via ajax
*
* Reimplemented to use different columns depending on discipline
*
* @param _data route data object
*/
Resultlist.prototype.handleResponse = function (_data) {
var detail = this.json_url.match(/detail=([^&]+)/);
switch (_data.discipline) {
case "speedrelay":
this.result_cols = !detail
? {
// default detail
result_rank: this.lang("Rank"),
team_name: this.lang("Teamname"),
"athletes/0/lastname": this.lang("Athlete #1"),
"athletes/1/lastname": this.lang("Athlete #2"),
"athletes/2/lastname": this.lang("Athlete #3"),
result: this.lang("Sum"),
}
: detail[1] == "1"
? {
// detail=1
result_rank: this.lang("Rank"),
team_name: this.lang("Teamname"),
//'team_nation': this.lang('Nation'),
"athletes/0/lastname": {
label: this.lang("Athlete #1"),
colspan: 3,
},
"athletes/0/firstname": "",
"athletes/0/result_time": "",
"athletes/1/lastname": {
label: this.lang("Athlete #2"),
colspan: 3,
},
"athletes/1/firstname": "",
"athletes/1/result_time": "",
"athletes/2/lastname": {
label: this.lang("Athlete #3"),
colspan: 3,
},
"athletes/2/firstname": "",
"athletes/2/result_time": "",
result: this.lang("Sum"),
}
: {
// detail=0
result_rank: this.lang("Rank"),
team_name: this.lang("Teamname"),
team_nation: this.lang("Nation"),
result: this.lang("Sum"),
};
break;
case "ranking":
this.result_cols = {
result_rank: this.lang("Rank"),
lastname: { label: this.lang("Name"), colspan: 2 },
firstname: "",
nation: this.lang("Nation"),
points: this.lang("Points"),
result: this.lang("Result"),
};
// default columns for SUI ranking with NO details
if ((!detail || detail[1] == "0") && _data.nation == "SUI") {
this.result_cols = {
result_rank: this.lang("Rank"),
lastname: { label: this.lang("Name"), colspan: 2 },
firstname: "",
birthyear: this.lang("Agegroup"),
city: this.lang("City"),
federation: "Sektion",
rgz: "Regionalzentrum",
points: this.lang("Points"),
result: this.lang("Result"),
};
}
if (
(!detail || detail[1] == "0") &&
_data.participants[0] &&
_data.participants[0].result_rank
) {
delete this.result_cols.result;
// allow to click on points to show single results
this.result_cols.points = {
label: this.result_cols.points,
url: location.href + "&detail=1",
};
// add calculation to see-also links
if (typeof _data.see_also == "undefined") _data.see_also = [];
_data.see_also.push({
name: this.lang("calculation of this ranking"),
url: location.href + "&detail=1",
});
}
break;
default:
// default columns for SUI ranking with NO details
if ((!detail || detail[1] == "0") && _data.nation == "SUI") {
this.result_cols = {
result_rank: this.lang("Rank"),
lastname: { label: this.lang("Name"), colspan: 2 },
firstname: "",
birthyear: this.lang("Agegroup"),
city: this.lang("City"),
federation: this.lang("Sektion"),
rgz: this.lang("Regionalzentrum"),
result: this.lang("Result"),
};
} else {
this.result_cols =
detail && detail[1] == "0"
? {
result_rank: this.lang("Rank"),
lastname: { label: this.lang("Name"), colspan: 2 },
firstname: "",
nation: this.lang("Nation"),
result: this.lang("Result"),
}
: _data.discipline == "speed" && _data.route_order == "0"
? {
result_rank: this.lang("Rank"),
lastname: { label: this.lang("Name"), colspan: 2 },
firstname: "",
nation: this.lang("Nation"),
start_number: this.lang("StartNr"),
result: this.lang("Result"),
result_l: this.lang("Result (A)"),
result_r: this.lang("Result (B)"),
}
: {
result_rank: this.lang("Rank"),
lastname: { label: this.lang("Name"), colspan: 2 },
firstname: "",
nation: this.lang("Nation"),
start_number: this.lang("StartNr"),
result: this.lang("Result"),
};
}
// for boulder heats use new display, but not for general result!
if (
_data.discipline.substr(0, 7) == "boulder" &&
_data.route_order != -1
) {
delete this.result_cols.result;
var that = this;
var num_problems = parseInt(_data.route_num_problems);
this.result_cols.boulder = function (_data, _tag) {
return that.getBoulderResult.call(that, _data, _tag, num_problems);
};
//Resultlist.prototype.getBoulderResult;
if (!detail || detail[1] != 0)
this.result_cols.result = this.lang("Sum");
}
break;
}
// remove start-number column if no start-numbers used (determined on first participant only)
if (
typeof this.result_cols.start_number != "undefined" &&
!_data.participants[0].start_number
) {
delete this.result_cols.start_number;
}
Startlist.prototype.handleResponse.call(this, _data);
align_td_nbsp("table.DrTable td");
if (
_data.discipline == "ranking" &&
!_data.error &&
((detail && detail[1] == "1") || !_data.participants[0].result_rank) &&
(_data.max_comp || _data.max_disciplines)
) {
var tfoot = jQuery(document.createElement("tfoot"));
jQuery(this.table.dom).append(tfoot);
var th = jQuery(document.createElement("th"));
tfoot.append(jQuery(document.createElement("tr")).append(th));
var cols = 0;
for (var c in this.result_cols) cols++;
th.attr("colspan", cols);
th.attr("class", "footer");
var max_disciplines = "";
if (_data.max_disciplines) {
for (var discipline in _data.max_disciplines) {
max_disciplines +=
(max_disciplines ? ", " : "") +
discipline[0].toUpperCase() +
discipline.slice(1) +
": " +
_data.max_disciplines[discipline];
}
}
if (_data.nation) {
th.html(
(_data.max_comp
? "Für " +
(_data.cup ? "den " + _data.cup.name : "die Rangliste") +
" zählen die " +
_data.max_comp +
" besten Ergebnisse. "
: "") +
(max_disciplines
? " Maximal zählende Ergebnisse pro Disziplin: " +
max_disciplines +
". "
: "") +
"Nicht zählende Ergebnisse sind eingeklammert. " +
(_data.min_disciplines
? "<br/>Teilnahme an mindestens " +
_data.min_disciplines +
" Disziplinen ist erforderlich. "
: "") +
(_data.drop_equally
? "Streichresultate erfolgen in allen Disziplinen gleichmäßig. "
: "")
);
} else {
th.html(
(_data.max_comp
? _data.max_comp +
" best competition results are counting for " +
(_data.cup ? _data.cup.name : "the ranking") +
". "
: "") +
(max_disciplines
? "Maximum number of counting results per discipline: " +
max_disciplines +
". "
: "") +
"Not counting points are in brackets. " +
(_data.min_disciplines
? "<br/>Participation in at least " +
_data.min_disciplines +
" disciplines is required."
: "") +
(_data.drop_equally
? "Not counting results are selected from all disciplines equally."
: "")
);
}
}
if (
_data.statistics &&
(_data.discipline == "selfscore" || this.json_url.match("&stats="))
) {
if (!jQuery("#jqplot-css").length) {
var ranking_url = this.json_url.replace(/json.php.*$/, "");
var jqplot_url =
this.json_url.replace(/ranking\/json.php.*$/, "") +
"vendor/npm-asset/as-jqplot/dist/";
jQuery("<link/>", {
id: "jqplot-css",
href: jqplot_url + "jquery.jqplot.min.css",
type: "text/css",
}).appendTo("head");
var load = [
jqplot_url + "jquery.jqplot.min.js",
// not sure why bar-renderer does not work :(
//jqplot_url+'plugins/jqplot.barRenderer.min.js',
jqplot_url + "plugins/jqplot.highlighter.min.js",
ranking_url + "js/dr_statistics.js?" + _data.dr_statistics,
];
for (var i = 0; i < load.length; ++i) {
load[i] = jQuery.ajax({
url: load[i],
dataType: "script",
cache: true, // no cache buster!
});
}
var container = this.container;
jQuery.when.apply(jQuery, load).done(function () {
dr_statistics(container, _data);
});
// dono why, but above done is not always executed, if files are already cached
window.setTimeout(function () {
typeof window.dr_statistics != "undefined" &&
dr_statistics(container, _data);
}, 100);
} else {
dr_statistics(this.container, _data);
}
}
};
/**
* Get DOM nodes for display of graphical boulder-result
*
* @param _data
* @param _tag 'th' for header, 'td' for data rows
* @param _num_problems
* @return DOM node
*/
Resultlist.prototype.getBoulderResult = function (
_data,
_tag,
_num_problems
) {
if (_tag == "th") return "Result";
var tag = document.createElement("div");
for (var i = 1; i <= _num_problems; ++i) {
var boulder = document.createElement("div");
var result = _data["boulder" + i];
if (result && result != "z0" && result != "b0") {
var top_tries = result.match(/t([0-9]+)/);
var bonus_tries = result.match(/(b|z)([0-9]+)/);
if (top_tries) {
boulder.className = "boulderTop";
var top_text = document.createElement("div");
top_text.className = "topTries";
jQuery(top_text).text(top_tries[1]);
jQuery(boulder).append(top_text);
} else {
boulder.className = "boulderBonus";
}
var bonus_text = document.createElement("div");
bonus_text.className = "bonusTries";
jQuery(bonus_text).text(bonus_tries[2]);
jQuery(boulder).append(bonus_text);
} else {
boulder.className = result ? "boulderNone" : "boulder";
}
jQuery(tag).append(boulder);
}
return tag;
};
return Resultlist;
})();
/**
* Results widget inheriting from DrBaseWidget
*/
var Results = (function () {
/**
* Constructor for results from given json url
*
* Table get appended to specified _container
*
* @param _container
* @param _json_url url for data to load
* @param {boolean} _no_navigation do not show competition chooser
*/
function Results(_container, _json_url, _no_navigation) {
DrBaseWidget.prototype.constructor.call(this, _container, _json_url);
this.no_navigation = _no_navigation;
this.update();
}
// inherite from DrBaseWidget
Results.prototype = new DrBaseWidget();
Results.prototype.constructor = Results;
/**
* Callback for loading data via ajax
*
* @param _data route data object
*/
Results.prototype.handleResponse = function (_data) {
this.columns = {
result_rank: this.lang("Rank"),
lastname: { label: this.lang("Name"), colspan: 2 },
firstname: "",
nation: this.lang("Nation"),
};
this.replace_nation(_data.display_athlete, _data.nation);
if (typeof this.table == "undefined") {
// competition chooser
if (!this.no_navigation) {
this.comp_chooser = jQuery(document.createElement("select"));
this.comp_chooser.addClass("compChooser");
this.container.append(this.comp_chooser);
var that = this;
this.comp_chooser.change(function (e) {
that.json_url = that.json_url.replace(
/comp=[^&]+/,
"comp=" + this.value
);
if (that.navigateTo) that.navigateTo(that.json_url);
else that.update();
});
}
// competition
this.comp_header = jQuery(document.createElement("h1"));
this.comp_header.addClass("compHeader");
this.container.append(this.comp_header);
// result date
this.comp_date = jQuery(document.createElement("h3"));
this.comp_date.addClass("resultDate");
this.container.append(this.comp_date);
} else {
jQuery(this.table.dom).remove();
if (!this.no_navigation) this.comp_chooser.empty();
this.comp_header.empty();
this.comp_date.empty();
}
// fill competition chooser
if (!this.no_navigation) {
var option = jQuery(document.createElement("option"));
option.text(this.lang("Select another competition ..."));
this.comp_chooser.append(option);
for (var i = 0; i < _data.competitions.length; ++i) {
var competition = _data.competitions[i];
if (_data.WetId == competition.WetId) continue; // we dont show current competition
option = jQuery(document.createElement("option"));
option.attr({ value: competition.WetId, title: competition.date_span });
option.text(competition.name);
this.comp_chooser.append(option);
}
}
this.comp_header.text(_data.name);
this.comp_date.text(_data.date_span);
for (var i = 0; i < _data.categorys.length; ++i) {
var cat = _data.categorys[i];
var that = this;
cat.click = function (e) {
that.showCompleteResult(e);
};
}
// create new table
this.table = new DrTable(
_data.categorys,
this.columns,
"result_rank",
true,
null,
this.navigateTo
);
this.container.append(this.table.dom);
this.seeAlso(_data.see_also);
};
/**
* Switch from Results (of all categories) to Resultlist (of a single category)
*
* @param e
*/
Results.prototype.showCompleteResult = function (e) {
this.container.empty();
this.container.removeClass("Results");
new Resultlist(
this.container,
this.json_url.replace(/\?.*$/, e.target.href.match(/\?.*$/)[0])
);
e.preventDefault();
};
return Results;
})();
/**
* Starters / registration widget inheriting from DrBaseWidget
*/
var Starters = (function () {
/**
* Constructor for results from given json url
*
* Table get appended to specified _container
*
* @param _container
* @param _json_url url for data to load
*/
function Starters(_container, _json_url) {
DrBaseWidget.prototype.constructor.call(this, _container, _json_url);
this.update();
}
// inherite from DrBaseWidget
Starters.prototype = new DrBaseWidget();
Starters.prototype.constructor = Starters;
/**
* Callback for loading data via ajax
*
* @param _data route data object
*/
Starters.prototype.handleResponse = function (_data) {
this.data = _data;
if (typeof this.table == "undefined") {
// competition
this.comp_header = jQuery(document.createElement("h1"));
this.comp_header.addClass("compHeader");
this.container.append(this.comp_header);
// result date
this.comp_date = jQuery(document.createElement("h3"));
this.comp_date.addClass("resultDate");
this.container.append(this.comp_date);
} else {
delete this.table;
this.comp_header.empty();
this.comp_date.empty();
}
this.comp_header.text(_data.name + " : " + _data.date_span);
if (_data.deadline)
this.comp_date.text(this.lang("Deadline") + ": " + _data.deadline);
this.table = jQuery(document.createElement("table")).addClass("DrTable");
this.container.append(this.table);
var thead = jQuery(document.createElement("thead"));
this.table.append(thead);
// create header row
var row = jQuery(document.createElement("tr"));
var th = jQuery(document.createElement("th"));
th.text(
typeof _data.federations != "undefined"
? this.lang("Federation")
: this.lang("Nation")
);
if (!this.json_url.match(/no_fed=1/)) row.append(th);
var cats = {};
for (var i = 0; i < _data.categorys.length; ++i) {
var th = jQuery(document.createElement("th"));
th.addClass("category");
th.text(_data.categorys[i].name);
row.append(th);
cats[_data.categorys[i].GrpId] = i;
}
thead.append(row);
var tbody = jQuery(document.createElement("tbody"));
this.table.append(tbody);
var fed;
this.fed_rows = [];
this.fed_rows_pos = [];
var num_competitors = 0;
for (var i = 0; i < _data.athletes.length; ++i) {
var athlete = _data.athletes[i];
// evtl. create new row for federation/nation
if (
(typeof fed == "undefined" || fed != athlete.reg_fed_id) &&
!this.json_url.match(/no_fed=1/)
) {
this.fillUpFedRows();
// reset fed rows to empty
this.fed_rows = [];
this.fed_rows_pos = [];
}
// find rows with space in column of category
var cat_col = cats[athlete.cat];
for (var r = 0; r < this.fed_rows.length; ++r) {
if (this.fed_rows_pos[r] <= cat_col) break;
}
if (r == this.fed_rows.length) {
// create a new fed-row
row = jQuery(document.createElement("tr"));
tbody.append(row);
th = jQuery(document.createElement("th"));
if (!this.json_url.match(/no_fed=1/)) row.append(th);
this.fed_rows.push(row);
this.fed_rows_pos.push(0);
if (typeof fed == "undefined" || fed != athlete.reg_fed_id) {
fed = athlete.reg_fed_id;
th.text(this.federation(athlete.reg_fed_id));
th.addClass("federation");
}
}
this.fillUpFedRow(r, cat_col);
// create athlete cell
var td = jQuery(document.createElement("td"));
td.addClass("athlete");
var lastname = jQuery(document.createElement("span"))
.addClass("lastname")
.text(athlete.lastname);
var firstname = jQuery(document.createElement("span"))
.addClass("firstname")
.text(athlete.firstname);
td.append(lastname).append(firstname);
this.fed_rows[r].append(td);
this.fed_rows_pos[r]++;
// do not count
if (athlete.cat != 120) num_competitors++;
}
this.fillUpFedRows();
var tfoot = jQuery(document.createElement("tfoot"));
this.table.append(tfoot);
var th = jQuery(document.createElement("th"));
tfoot.append(jQuery(document.createElement("tr")).append(th));
th.attr("colspan", 1 + _data.categorys.length);
th.text(
this.lang(
"Total of %1 athletes registered in all categories.",
num_competitors
)
);
};
/**
* Fill a single fed-row up to a given position with empty td's
*
* @param {number} _r row-number
* @param {number} _to column-number, default whole row
*/
Starters.prototype.fillUpFedRow = function (_r, _to) {
if (typeof _to == "undefined") _to = this.data.categorys.length;
while (this.fed_rows_pos[_r] < _to) {
var td = jQuery(document.createElement("td"));
this.fed_rows[_r].append(td);
this.fed_rows_pos[_r]++;
}
};
/**
* Fill up all fed rows with empty td's
*/
Starters.prototype.fillUpFedRows = function () {
for (var r = 0; r < this.fed_rows.length; ++r) {
this.fillUpFedRow(r);
}
};
/**
* Get name of federation specified by given id
*
* @param _fed_id
* @returns string with name
*/
Starters.prototype.federation = function (_fed_id) {
if (typeof this.data.federations == "undefined") {
return _fed_id; // nation of int. competition
}
for (var i = 0; i < this.data.federations.length; ++i) {
var fed = this.data.federations[i];
if (fed.fed_id == _fed_id) return fed.shortcut || fed.name;
}
};
return Starters;
})();
/**
* Profile widget inheriting from DrBaseWidget
*/
var Profile = (function () {
/**
* Constructor for profile from given json url
*
* Table get appended to specified _container
*
* @param _container
* @param _json_url url for data to load
* @param _template optional string with html-template
* @param _remove_leading_slash fix Joomla behavior of adding a slash to <a href="$$something$$"
*/
function Profile(_container, _json_url, _template, _remove_leading_slash) {
DrBaseWidget.prototype.constructor.call(this, _container, _json_url);
if (_template) this.template = _template;
if (typeof _remove_leading_slash == "undefined")
_remove_leading_slash = false;
this.pattern = new RegExp(
(_remove_leading_slash ? "/?" : "") + "\\$\\$([^$]+)\\$\\$",
"g"
);
this.pattern_results = new RegExp(
(_remove_leading_slash ? "/?" : "") + "\\$\\$results/N/([^$]+)\\$\\$",
"g"
);
this.container.empty();
this.bestResults = 12;
this.update();
}
// inherite from DrBaseWidget
Profile.prototype = new DrBaseWidget();
Profile.prototype.constructor = Profile;
/**
* Callback for loading data via ajax
*
* @param _data route data object
*/
Profile.prototype.handleResponse = function (_data) {
if (_data.error) {
var error = jQuery(document.createElement("h3"));
error.addClass("error");
error.text(_data.error);
this.container.replaceWith(error);
return;
}
// replace non-result data
var that = this;
var html = this.template.replace(
this.pattern,
function (match, placeholder) {
switch (placeholder) {
case "categoryChooser":
var select = '<select class="chooseCategory">\n';
if (typeof _data.categorys === "undefined") {
return "";
}
for (var i = 0; i < _data.categorys.length; ++i) {
var cat = _data.categorys[i];
select +=
'<option value="' +
cat.GrpId +
'"' +
(cat.GrpId == _data.GrpId ? " selected" : "") +
">" +
cat.name +
"</option>\n";
}
select += "</select>\n";
return select;
}
var parts = placeholder.split("/");
var data = _data;
for (var i = 0; i < parts.length; ++i) {
if (typeof data[parts[i]] == "undefined" || !data[parts[i]]) {
return parts[i] === "N" ? match : "";
}
data = data[parts[i]];
}
switch (placeholder) {
case "practice":
data +=
" " +
that.lang("years, since") +
" " +
(new Date().getFullYear() - data);
break;
case "height":
data += " cm";
break;
case "weight":
data += " kg";
break;
}
return data;
}
);
// replace result data
var bestResults = this.bestResults;
var that = this;
html = html.replace(/[\s]*<tr[\s\S]*?<\/tr>\n?/g, function (match) {
if (match.indexOf("$$results/N/") == -1) return match;
// find and mark N best results
var year = new Date().getFullYear();
var limits = [];
for (var i = 0; i < _data.results.length; ++i) {
var result = _data.results[i];
result.weight =
result.rank / 2 + (year - parseInt(result.date)) + 4 * !result.nation;
// maintain array of N best competitions (least weight)
if (
limits.length < bestResults ||
result.weight < limits[limits.length - 1]
) {
var limit = 0;
for (var l = 0; l < limits.length; ++l) {
limit = limits[l];
if (limit > result.weight) break;
}
if (limit < result.weight && l == limits.length - 1)
l = limits.length;
limits = limits
.slice(0, l)
.concat([result.weight])
.concat(limits.slice(l, bestResults - 1 - l));
}
}
var weight_limit = limits.pop();
var rows = "";
var l = 0;
for (var i = 0; i < _data.results.length; ++i) {
var result = _data.results[i];
if (
match.indexOf("$$results/N/weightClass$$") >= 0 &&
(result.weight > weight_limit || ++l > bestResults)
) {
result.weightClass = "profileResultHidden";
}
rows += match.replace(
that.pattern_results,
function (match, placeholder) {
switch (placeholder) {
case "cat_name+name":
return (
(result.GrpId != _data.GrpId ? result.cat_name + ": " : "") +
result.name
);
case "date":
return that.formatDate(result.date);
default:
return typeof result[placeholder] != "undefined"
? result[placeholder]
: "";
}
}
);
}
return rows;
});
this.container.html(html);
// remove links with empty href
this.container.find('a[href=""]').replaceWith(function () {
return jQuery(this).contents();
});
// remove images with empty src
this.container.find('img[src=""]').remove();
// hide rows with profileHideRowIfEmpty, if ALL td.profileHideRowIfEmpty are empty
this.container.find("tr.profileHideRowIfEmpty").each(function (index, row) {
var tds = jQuery(row).children("td.profileHideRowIfEmpty");
if (tds.length == tds.filter(":empty").length) {
jQuery(row).hide();
}
});
// install click handler from DrWidget
if (this.navigateTo)
this.container
.find('.profileData a:not(a[href^="javascript:"])')
.click(this.navigateTo);
// bind chooseCategory handler (works with multiple templates)
var that = this;
this.container.find("select.chooseCategory").change(function (e) {
that.chooseCategory.call(that, this.value);
e.stopImmediatePropagation();
return false;
});
};
/**
* toggle between best results and all results
*/
Profile.prototype.toggleResults = function () {
var hidden_rows = this.container.find("tr.profileResultHidden");
var display = hidden_rows.length
? jQuery(hidden_rows[0]).css("display")
: "none";
hidden_rows.css("display", display == "none" ? "table-row" : "none");
};
/**
* choose a given category for rankings
*
* @param {string} GrpId
*/
Profile.prototype.chooseCategory = function (GrpId) {
var cat_regexp = /([#&])cat=([^&]+)/;
function replace_cat(str, GrpId) {
if (str.match(cat_regexp)) {
return str.replace(cat_regexp, "$1cat=" + GrpId);
}
return str + "&cat=" + GrpId;
}
location.hash = replace_cat(location.hash, GrpId);
this.json_url = replace_cat(this.json_url, GrpId);
this.update();
};
/**
* Default template for Profile widget
*/
Profile.prototype.template =
"<div>\n" +
'<table class="profileHeader">\n' +
" <thead>\n" +
" <tr>\n" +
' <td class="profilePhoto"><img src="$$photo$$" border="0"></td>\n' +
" <td>\n" +
' <h1><a href="$$homepage$$" target="_blank">\n' +
' <span class="firstname">$$firstname$$</span>\n' +
' <span class="lastname">$$lastname$$</span>\n' +
" </a></h1>\n" +
' <h2 class="profileNation">$$nation$$</h1>\n' +
' <h3 class="profileFederation"><a href="$$fed_url$$" target="_blank">$$federation$$</a></h1>\n' +
" </td>\n" +
' <td class="profileLogo"><a href="http://www.digitalROCK.de" target=_blank><img src="http://www.digitalrock.de/dig_rock-155x100.png" title="digital ROCK\'s Homepage" /></a></td>\n' +
" </tr>\n" +
" </thead>\n" +
"</table>\n" +
'<table cols="6" class="profileData">\n' +
" <thead>\n" +
" <tr>\n" +
" <td>age:</td>\n" +
' <td class="profileAge">$$age$$</td>\n' +
" <td>year of birth:</td>\n" +
' <td colspan="3" class="profileBirthdate">$$birthdate$$</td>\n' +
" </tr>\n" +
' <tr class="profileHideRowIfEmpty">\n' +
' <td colspan="2"></td>\n' +
" <td>place of birth:</td>\n" +
' <td colspan="3" class="profileBirthplace profileHideRowIfEmpty">$$birthplace$$</td>\n' +
" </tr>\n" +
' <tr class="profileHideRowIfEmpty">\n' +
" <td>height:</td>\n" +
' <td class="profileHeight profileHideRowIfEmpty">$$height$$</td>\n' +
" <td>weight:</td>\n" +
' <td colspan="3" class="profileWeight profileHideRowIfEmpty">$$weight$$</td>\n' +
" </tr>\n" +
' <tr class="profileMarginTop profileHideRowIfEmpty">\n' +
" <td>address:</td>\n" +
' <td colspan="2" class="profileCity profileHideRowIfEmpty">$$postcode$$ $$city$$</td>\n' +
' <td colspan="3" class="profileStreet profileHideRowIfEmpty">$$street$$</td>\n' +
" </tr>\n" +
' <tr class="profileMarginTop profileHideRowIfEmpty">\n' +
' <td colspan="2">practicing climbing for:</td>\n' +
' <td colspan="4" class="profilePractice profileHideRowIfEmpty">$$practice$$</td>\n' +
" </tr>\n" +
' <tr class="profileHideRowIfEmpty">\n' +
' <td colspan="2">professional climber (if not, profession):</td>\n' +
' <td colspan="4" class="profileProfessional profileHideRowIfEmpty">$$professional$$</td>\n' +
" </tr>\n" +
' <tr class="profileHideRowIfEmpty">\n' +
' <td colspan="2">other sports practiced:</td>\n' +
' <td colspan="4" class="profileOtherSports profileHideRowIfEmpty">$$other_sports$$</td>\n' +
" </tr>\n" +
' <tr class="profileMarginTop profileHideRowIfEmpty">\n' +
' <td colspan="6" class="profileFreetext profileHideRowIfEmpty">$$freetext$$</td>\n' +
" </tr>\n" +
' <tr class="profileMarginTop">\n' +
' <td colspan="6">Category: $$categoryChooser$$</td>\n' +
" </tr>\n" +
" <tr>\n" +
' <td colspan="2" class="profileRanglist"><a href="$$rankings/0/url$$">$$rankings/0/name$$</a>:</td>\n' +
' <td class="profileRank">$$rankings/0/rank$$</td>\n' +
' <td colspan="2" class="profileRanglist"><a href="$$rankings/1/url$$">$$rankings/1/name$$</a>:</td>\n' +
' <td class="profileRank">$$rankings/1/rank$$</td>\n' +
" </tr>\n" +
" <tr>\n" +
' <td colspan="2" class="profileRanglist"><a href="$$rankings/2/url$$">$$rankings/2/name$$</a>:</td>\n' +
' <td class="profileRank">$$rankings/2/rank$$</td>\n' +
' <td colspan="2" class="profileRanglist"><a href="$$rankings/3/url$$">$$rankings/3/name$$</a>:</td>\n' +
' <td class="profileRank">$$rankings/3/rank$$</td>\n' +
" </tr>\n" +
' <tr class="profileResultHeader profileMarginTop">\n' +
' <td colspan="6"><a href="javascript:widget.widget.toggleResults()" title="click to toggle between best results and all results">best results / all results:</a></td>\n' +
" </tr>\n" +
" </thead>\n" +
" <tbody>\n" +
' <tr class="profileResult $$results/N/weightClass$$">\n' +
' <td class="profileResultRank">$$results/N/rank$$</td>\n' +
' <td colspan="4" class="profileResultName"><a href="$$results/N/url$$">$$results/N/cat_name+name$$</a></td>\n' +
' <td class="profileResultDate">$$results/N/date$$</td>\n' +
" </tr>\n" +
" </tbody>\n" +
"</table>\n" +
"</div>\n";
return Profile;
})();
/**
* ResultTemplate widget inheriting from DrBaseWidget
*/
var ResultTemplate = (function () {
/**
* Constructor for ResultTemplate from given json url
*
* Table get appended to specified _container
*
* @param _container
* @param _json_url url for data to load
* @param _template optional string with html-template
*/
function ResultTemplate(_container, _json_url, _template) {
DrBaseWidget.prototype.constructor.call(this, _container, _json_url);
if (_template) this.template = _template;
else this.template = this.container.html();
this.container.empty();
this.update();
}
// inherite from DrBaseWidget
ResultTemplate.prototype = new DrBaseWidget();
ResultTemplate.prototype.constructor = ResultTemplate;
/**
* Callback for loading data via ajax
*
* @param _data route data object
*/
ResultTemplate.prototype.handleResponse = function (_data) {
// if route is NOT offical, update list every 10 sec
if (!_data.route_result && typeof this.update_handle == "undefined") {
var list = this;
this.update_handle = window.setInterval(function () {
list.update();
}, 10000);
}
// if route is offical stop reload
else if (_data.route_result && this.update_handle) {
window.clearInterval(this.update_handle);
delete this.update_handle;
}
// replace non-result data
var pattern = /\$\$([^$]+)\$\$/g;
var html = this.template.replace(pattern, function (match, placeholder) {
var parts = placeholder.split("/");
var data = _data;
for (var i = 0; i < parts.length; ++i) {
if (typeof data[parts[i]] == "undefined" || !data[parts[i]]) {
return parts[i] === "N" ? match : "";
}
data = data[parts[i]];
}
switch (placeholder) {
}
return data;
});
// replace result data
pattern = /\$\$participants\/N\/([^$]+)\$\$/g;
html = html.replace(/[\s]*<tr[\s\S]*?<\/tr>\n?/g, function (match) {
if (match.indexOf("$$participants/N/") == -1) return match;
var rows = "";
for (var i = 0; i < _data.participants.length; ++i) {
var result = _data.participants[i];
rows += match.replace(pattern, function (match, placeholder) {
switch (placeholder) {
default:
return typeof result[placeholder] != "undefined"
? result[placeholder]
: "";
}
});
}
return rows;
});
// replace container
this.container.html(html);
};
return ResultTemplate;
})();
/**
* Competitions / calendar widget inheriting from DrBaseWidget
*/
var Competitions = (function () {
/**
* Constructor for ompetitions / calendar from given json url
*
* Table get appended to specified _container
*
* @param _container
* @param _json_url url for data to load
* @param _filters object with filters and optional _comp_url
* {string} _filters._comp_url url to use as link with added WetId for competition name
*/
function Competitions(_container, _json_url, _filters) {
DrBaseWidget.prototype.constructor.call(this, _container, _json_url);
if (typeof _filters != "undefined") {
this.filters = _filters;
if (typeof _filters._comp_url != "undefined") {
this.comp_url = _filters._comp_url;
delete this.filters._comp_url;
}
if (typeof _filters._comp_url_label != "undefined") {
this.comp_url_label = _filters._comp_url_label;
delete this.filters._comp_url_label;
}
}
this.year_regexp = /([&?])year=(\d+)/;
this.update();
}
// inherite from DrBaseWidget
Competitions.prototype = new DrBaseWidget();
Competitions.prototype.constructor = Competitions;
/**
* Callback for loading data via ajax
*
* @param _data route data object
*/
Competitions.prototype.handleResponse = function (_data) {
this.container.empty();
var year = this.json_url.match(this.year_regexp);
year = year ? parseInt(year[2]) : new Date().getFullYear();
var h1 = jQuery(document.createElement("h1")).text(
this.lang("Calendar") + " " + year
);
this.container.append(h1);
var filter = jQuery(document.createElement("div")).addClass("filter");
var select = jQuery(document.createElement("select")).attr("name", "year");
var years = _data.years || [year + 1, year, year - 1];
for (var i = 0; i < years.length; ++i) {
var y = years[i];
var option = jQuery(document.createElement("option")).attr("value", y);
option.text(y);
if (year == y) option.attr("selected", "selected");
select.append(option);
}
var that = this;
select.change(function (e) {
that.changeYear(this.value);
});
select.attr("style", "margin-right: 5px");
filter.append(select);
if (
typeof this.filters != "undefied" &&
!jQuery.isEmptyObject(this.filters)
) {
select = jQuery(document.createElement("select")).attr("name", "filter");
for (var f in this.filters) {
var option = jQuery(document.createElement("option")).attr(
"value",
this.filters[f]
);
option.text(f);
if (decodeURI(this.json_url).indexOf(this.filters[f]) != -1)
option.attr("selected", "selected");
select.append(option);
}
select.change(function (e) {
that.changeFilter(this.value);
});
filter.append(select);
}
this.container.append(filter);
var competitions = jQuery(document.createElement("div")).addClass(
"competitions"
);
this.container.append(competitions);
var now = new Date();
// until incl. Wednesday (=3) we display competitions from last week first, after that from this week
var week_to_display = now.getWeek() - (now.getDay() <= 3 ? 1 : 0);
var closest, closest_dist;
for (var i = 0; i < _data.competitions.length; ++i) {
var competition = _data.competitions[i];
var comp_div = jQuery(document.createElement("div")).addClass(
"competition"
);
var title = jQuery(document.createElement("div"))
.addClass("title")
.text(competition.name);
if (this.comp_url) {
title = jQuery(document.createElement("a"))
.attr({ href: this.comp_url + competition.WetId })
.append(title);
}
comp_div.append(title);
comp_div.append(
jQuery(document.createElement("div"))
.addClass("date")
.text(competition.date_span)
);
var cats_ul = jQuery(document.createElement("ul")).addClass("cats");
var have_cats = false;
var links = {
homepage: this.lang("Event Website"),
info: this.lang("Regulation"),
info2: this.lang("Info Sheet"),
startlist: this.lang("Startlist"),
result: this.lang("Result"),
};
// add comp_url as first link with given label
if (this.comp_url && this.comp_url_label) {
links = jQuery.extend({ comp_url: this.comp_url_label }, links);
competition.comp_url = this.comp_url + competition.WetId;
}
if (typeof competition.cats == "undefined") competition.cats = [];
for (var c = 0; c < competition.cats.length; ++c) {
var cat = competition.cats[c];
var url = "";
if (typeof cat.status != "undefined") {
switch (cat.status) {
case 4: // registration
links.starters = this.lang("Starters");
competition.starters =
"#!type=starters&comp=" + competition.WetId;
break;
case 2: // startlist in result-service
case 1: // result in result-service
case 0: // result in ranking (ToDo: need extra export, as it might not be in result-service)
url = "#!comp=" + competition.WetId + "&cat=" + cat.GrpId;
break;
}
}
var cat_li = jQuery(document.createElement("li"));
if (url != "") {
var a = jQuery(document.createElement("a")).attr("href", url);
a.text(cat.name);
if (this.navigateTo) a.click(this.navigateTo);
cat_li.append(a);
} else {
cat_li.text(cat.name);
}
cats_ul.append(cat_li);
have_cats = true;
}
var links_ul = jQuery(document.createElement("ul")).addClass("links");
var have_links = false;
for (var l in links) {
if (typeof competition[l] == "undefined" || competition[l] === null)
continue;
var a = jQuery(document.createElement("a"));
a.attr("href", competition[l]);
if (l == "comp_url" && this.comp_url[0] == "/");
else if (l != "starters") a.attr("target", "_blank");
else if (this.navigateTo) a.click(this.navigateTo);
a.text(links[l]);
links_ul.append(
jQuery(document.createElement("li"))
.addClass(l + "Link")
.append(a)
);
have_links = true;
}
if (have_links) comp_div.append(links_ul);
if (have_cats) comp_div.append(cats_ul);
competitions.append(comp_div);
var dist = Math.abs(
new Date(competition.date).getWeek() - week_to_display
);
if (typeof closest_dist == "undefined" || dist < closest_dist) {
closest_dist = dist;
closest = comp_div[0];
}
}
if (closest && year == new Date().getFullYear()) {
// need to delay scrolling a bit, layout seems to need some time
window.setTimeout(function () {
closest.parentElement.scrollTop =
closest.offsetTop + closest.parentElement.offsetTop;
}, 100);
}
};
Competitions.prototype.changeYear = function (year) {
if (this.json_url.match(this.year_regexp)) {
this.json_url = this.json_url.replace(this.year_regexp, "$1year=" + year);
} else {
if (this.json_url.substr(-1) != "?")
this.json_url += this.json_url.indexOf("?") == -1 ? "?" : "&";
this.json_url += "year=" + year;
}
if (this.navigateTo) {
this.navigateTo(this.json_url);
} else {
this.update();
}
};
Competitions.prototype.changeFilter = function (filter) {
if (this.json_url.indexOf("?") == -1) {
this.json_url += "?" + filter;
} else {
var year = this.json_url.match(this.year_regexp);
this.json_url = this.json_url.replace(
/\?.*$/,
"?" + (year && year[2] ? "year=" + year[2] + "&" : "") + filter
);
}
if (this.navigateTo) {
this.navigateTo(this.json_url);
} else {
this.update();
}
};
return Competitions;
})();
/**
* Aggregated rankings widget inheriting from DrBaseWidget
*/
var Aggregated = (function () {
/**
* Constructor for aggregated rankings (nat. team ranking, sektionenwertung, ...) from given json url
*
* Table get appended to specified _container
*
* @param _container
* @param _json_url url for data to load
*/
function Aggregated(_container, _json_url) {
DrBaseWidget.prototype.constructor.call(this, _container, _json_url);
this.update();
}
// inherite from DrBaseWidget
Aggregated.prototype = new DrBaseWidget();
Aggregated.prototype.constructor = Aggregated;
/**
* Callback for loading data via ajax
*
* @param _data route data object
*/
Aggregated.prototype.handleResponse = function (_data) {
var that = this;
// if we are not controlled by DrWidget, install our own navigation
if (!this.navigateTo) {
this.navigateTo = function (e) {
document.location.hash = this.href.replace(/^.*#!/, "");
e.preventDefault();
};
this.update = function () {
that.json_url =
that.json_url.replace(/&(cup|comp|cat)=[^&]+/, "") +
"&" +
document.location.hash.substr(1);
DrBaseWidget.prototype.update.call(that);
};
this.installPopState();
}
this.columns = {
rank: this.lang("Rank"),
nation: { label: _data.aggregated_name, colspan: 2 },
name: _data.aggregated_name,
points: { label: this.lang("Points") },
};
if (location.hash.indexOf("detail=1") == -1) {
this.columns.points.click = function (e) {
var hidden_cols = that.container.find(".result,.calculationHidden");
var display = hidden_cols.length
? jQuery(hidden_cols[0]).css("display")
: "none";
hidden_cols.css("display", display == "none" ? "table-cell" : "none");
location.hash += "&detail=1";
e.preventDefault();
};
// add calculation to see-also links
if (typeof _data.see_also == "undefined") _data.see_also = [];
_data.see_also.push({
name: "calculation of this ranking",
url: location.href + "&detail=1",
});
}
if (_data.aggregate_by != "nation") delete this.columns.nation;
if (typeof this.table == "undefined") {
this.ranking_name = jQuery(document.createElement("h1")).addClass(
"rankingName"
);
this.container.append(this.ranking_name);
// cup or competition
this.header = jQuery(document.createElement("h2")).addClass(
"rankingHeader"
);
this.container.append(this.header);
// category names
this.header2 = jQuery(document.createElement("h3")).addClass(
"rankingHeader2"
);
this.container.append(this.header2);
} else {
jQuery(this.table.dom).remove();
this.header.empty();
this.header2.empty();
}
this.ranking_name.text(_data.name);
this.header.text(_data.cup_name || _data.comp_name);
// if we filter by cat display categories as 2. header
if (_data.cat_filter) {
if (_data.cat_name) {
// category name given, use it but upcase and space it
this.header2.text(_data.cat_name.toUpperCase().split("").join(" "));
} else {
var names = "";
for (var i in _data.categorys) {
names += (names ? ", " : "") + _data.categorys[i].name;
}
this.header2.text(names);
}
}
// make _data available to other methods
this.data = _data;
var comps = [];
for (var c in _data.competitions) comps.push(_data.competitions[c]);
// use competition columns for more then one comp. and international or SUI
if (comps.length > 1 && (!_data.nation || _data.nation == "SUI")) {
comps.sort(function (a, b) {
return a.date < b.date ? 1 : -1;
});
for (var c = 0; c < comps.length; ++c) {
this.columns["result" + comps[c].WetId] = function (
_data,
_tag,
_name
) {
return that.comp_column.call(that, _data, _tag, _name);
};
}
} // otherwise use category header
else {
var cats = [];
for (var c in _data.categorys) cats.push(_data.categorys[c]);
cats.sort(function (a, b) {
return a.name < b.name ? -1 : 1;
});
for (var c = 0; c < cats.length; ++c) {
this.columns["result" + cats[c].GrpId] = function (_data, _tag, _name) {
return that.cat_column.call(that, _data, _tag, _name);
};
}
}
if (!_data.use_cup_points) {
// display all ranking points with 2 digits
for (var f = 0; f < _data.federations.length; ++f) {
_data.federations[f].points = _data.federations[f].points.toFixed(2);
}
}
// create new table
this.table = new DrTable(
_data.federations,
this.columns,
false,
true,
null,
this.navigateTo
);
// add table footer with note about how many results are counting
var tfoot = jQuery(document.createElement("tfoot"));
jQuery(this.table.dom).append(tfoot);
var th = jQuery(document.createElement("th")).addClass("result");
if (this.json_url.indexOf("detail=1") == -1)
th.addClass("calculationHidden");
tfoot.append(jQuery(document.createElement("tr")).append(th));
var cols = 0;
for (var c in this.columns) cols++;
th.attr("colspan", cols);
th.text(
this.lang(
"For %1 %2 best results per competition and category are counting.",
_data.name,
_data.best_results
) +
" " +
this.lang("Not counting results are in brackets.")
);
this.container.append(this.table.dom);
this.seeAlso(_data.see_also);
};
/**
* Display a competition column
*
* @param _data
* @param _tag tag 'th' for header or 'td' for data row
* @param _name column-name
*/
Aggregated.prototype.comp_column = function (_data, _tag, _name) {
var id = _name.substr(6);
var ret = { className: "result" };
if (this.json_url.indexOf("detail=1") == -1)
ret.className += " calculationHidden";
if (_tag == "th") {
// use comp. shortcut plus date as column header
ret.label =
(this.data.competitions[id].short ||
this.data.competitions[id].name.replace(/^.* - /, "")) +
"\n" +
this.formatDate(this.data.competitions[id].date);
// add comp to url evtl. replacing cup
ret.url =
location.href.indexOf("cup=") == -1
? location.href +
(location.href.indexOf("#!") == -1 ? "#!" : "&") +
"comp=" +
id
: location.href.replace(/(cup)=[^&]+/, "comp=" + id);
// nat. team ranking selects a cat, if none given, need to add it to not get a different selected
if (this.data.cat_filter && ret.url.indexOf("cat=") == -1)
ret.url += "&cat=" + this.data.cat_filter;
// keep in detailed view
if (ret.url.indexOf("detail=1") == -1) ret.url += "&detail=1";
ret.title = this.data.competitions[id].name;
} else if (
this.data.cat_filter &&
this.data.cat_filter.indexOf(",") == -1
) {
ret.label = "";
ret.nodes = this.results(id, "WetId", _data.counting);
} else {
ret.label = _data.comps[id];
}
return ret;
};
/**
* Display a category column
*
* @param _data
* @param _tag tag 'th' for header or 'td' for data row
* @param _name column-name
*/
Aggregated.prototype.cat_column = function (_data, _tag, _name) {
var id = _name.substr(6);
var ret = { className: "result" };
if (this.json_url.indexOf("detail=1") == -1)
ret.className += " calculationHidden";
if (_tag == "th") {
ret.label = this.data.categorys[id].name;
// get less wide headers by inserting a newline
ret.label = ret.label
.replace(/(lead|speed|boulder)/, "\n$1")
.replace(/(männliche|weibliche|male|female) */, "$1\n");
if (!this.data.comp_filter && !this.data.cat_filter) {
ret.url = location.href + "&cat=" + id;
// keep in detailed view
if (ret.url.indexOf("detail=1") == -1) ret.url += "&detail=1";
}
} else if (!this.data.comp_filter && !this.data.cat_filter) {
// sektionenwertung
var points = 0.0;
for (var r = 0; r < _data.counting.length; ++r) {
var result = _data.counting[r];
if (result.GrpId == id) {
points += result.points;
}
}
ret.label = points.toFixed(2);
} else {
ret.label = "";
ret.nodes = this.results(id, "GrpId", _data.counting);
}
return ret;
};
Aggregated.prototype.results = function (_id, _attr, _results) {
var cols = ["rank", "lastname", "firstname", "points"];
var nodes;
for (var r = 0; r < _results.length; ++r) {
var result = _results[r];
if (result[_attr] == _id) {
if (typeof nodes == "undefined") {
nodes = jQuery(document.createElement("div")).addClass("resultRows");
}
var div = jQuery(document.createElement("div")).addClass("resultRow");
for (var c = 0; c < cols.length; ++c) {
var col = cols[c];
var span = jQuery(document.createElement("span")).addClass(col);
span.text(
col != "points" || this.data.use_cup_points
? result[col]
: result[col].toFixed(2)
);
div.append(span);
}
nodes.append(div);
}
}
return nodes;
};
return Aggregated;
})();
/**
* Universal widget to display all data specified by _json_url or location
*/
var DrWidget = (function () {
/**
* call appropriate widget to display data specified by _json_url or location
*
* @param _container
* @param _json_url url for data to load
* @param _arg3 object with widget specific 3. argument, eg. { Competitions: {filters}, Profile: 'template-id' }
*/
function DrWidget(_container, _json_url, _arg3) {
DrBaseWidget.prototype.constructor.call(this, _container, _json_url);
this.arg3 = _arg3 || {};
var matches = this.json_url.match(/\?.*$/);
this.update(matches ? matches[0] : null);
// install this.update as PopState handler
this.installPopState();
}
// inherit from DrBaseWidget
DrWidget.prototype = new DrBaseWidget();
DrWidget.prototype.constructor = DrWidget;
/**
* Navigate to a certain result-page
*
* @param _params default if not specified first location.hash then location.search
* @param {DrBaseWidget} _widget to clear evtl. pending update timer
*/
DrWidget.prototype.navigateTo = function (_params, _widget) {
// clear pending update timer of widget, to stay on new widget we navigate to now
if (_widget && _widget.update_handle) {
window.clearTimeout(_widget.update_handle);
delete _widget.update_handle;
}
delete this.prevent_initial_pop;
// check if we have an ordinary link without hash or query, eg. result PDF
if (!_params.match(/(#|\?)/)) {
document.location = _params;
}
var params = "!" + _params.replace(/^.*(#!|#|\?)/, "");
// update location hash, to reflect current page-content
if (document.location.hash != "#" + params) document.location.hash = params;
};
DrWidget.prototype.update = function (_params) {
var params = _params || location.hash || location.search;
params = params.replace(/^.*(#!|#|\?)/, "");
this.json_url = this.json_url.replace(/\?.*$/, "") + "?" + params;
// check which widget is needed to render requested content
function hasParam(_param, _value) {
if (typeof _value == "undefined") _value = "";
return params.indexOf(_param + "=" + _value) != -1;
}
var widget;
if (
hasParam("type", "nat_team_ranking") ||
hasParam("type", "sektionenwertung") ||
hasParam("type", "regionalzentren")
) {
widget = "Aggregated";
} else if (hasParam("person")) {
widget = "Profile";
} else if (hasParam("cat") && !hasParam("comp")) {
widget = "Resultlist"; // ranking uses Resultlist!
} else if (hasParam("nation") || !hasParam("comp")) {
widget = "Competitions";
} else if (hasParam("comp") && hasParam("type", "starters")) {
widget = "Starters";
} else if ((hasParam("comp") && !hasParam("cat")) || hasParam("filter")) {
widget = "Results";
} else if (hasParam("type", "startlist")) {
widget = "Startlist";
} else {
widget = "Resultlist";
}
// check if widget is currently instancated and only need to update or need to be instancated
if (
typeof this.widget == "undefined" ||
this.widget.constructor != window[widget]
) {
this.container.html(""); // following .empty() does NOT work in IE8 Grrrr
this.container.empty();
// do a new of objects whos name is stored in widget
this.widget = Object.create(window[widget].prototype);
// Object.create, does NOT call constructor, so do it now manually
window[widget].call(
this.widget,
this.container,
this.json_url,
this.arg3[widget]
);
var that = this;
this.widget.navigateTo = function (e) {
if (typeof e == "string") {
that.navigateTo(e, that.widget);
} else {
that.navigateTo(this.href, that.widget);
e.preventDefault();
}
};
} else {
this.widget.json_url = this.json_url;
this.widget.update();
}
};
return DrWidget;
})();
/**
* Widget to let user choose a competition and category to show its result
*
* Chooser selectboxes stay visible, when user navigates in result eg. to show a profile
*/
var ResultChooser = (function () {
/**
* call appropriate widget to display data specified by _json_url or location
*
* @param _container
* @param _json_url url for data to load
* @param {object} _arg3 object with 3. parameter to pass to widget used to display results
*/
function ResultChooser(_container, _json_url, _arg3) {
DrBaseWidget.prototype.constructor.call(
this,
_container,
_json_url.replace(/&cat=[^&]+/, "")
);
this.arg3 = _arg3 || {
Results: true, // show NO own navigation
Startlist: true,
Resultlist: true,
};
var matches = this.json_url.match(/\?.*$/);
this.update(matches ? matches[0] : null);
this.comp_chooser = this.cat_chooser = undefined;
var matches = _json_url.match(/&cat=([^&]+)/);
this.cat = matches ? matches[1] : undefined;
this.widget = undefined;
// install this.update as PopState handler
//this.installPopState();
}
// inherit from DrBaseWidget
ResultChooser.prototype = new DrBaseWidget();
ResultChooser.prototype.constructor = ResultChooser;
/**
* Callback for loading data via ajax
*
* @param _data route data object
*/
ResultChooser.prototype.handleResponse = function (_data) {
if (!this.comp_chooser) {
// competition chooser
this.comp_chooser = jQuery(document.createElement("select"));
this.comp_chooser.addClass("compChooser");
this.container.append(this.comp_chooser);
var that = this;
this.comp_chooser.change(function (e) {
that.json_url = that.json_url
.replace(/comp=[^&]+/, "comp=" + this.value)
.replace(/&cat=[^&]+/, "");
that.update();
});
this.cat_chooser = jQuery(document.createElement("select"));
this.cat_chooser.addClass("catChooser");
this.container.append(this.cat_chooser);
this.cat_chooser.change(function (e) {
that.cat = this.value;
that.json_url = that.json_url.replace(
/comp=[^&]+/,
"comp=" + that.comp_chooser.val()
);
if (!that.cat) that.json_url = that.json_url.replace(/&cat=[^&]+/, "");
else if (that.json_url.search("cat=") == -1)
that.json_url += "&cat=" + this.value;
else
that.json_url = that.json_url.replace(
/cat=[^&]+/,
"cat=" + this.value
);
that.widget.navigateTo(that.json_url);
});
} else {
this.comp_chooser.empty();
this.cat_chooser.empty();
}
// fill competition chooser
for (var i = 0; i < _data.competitions.length; ++i) {
var competition = _data.competitions[i];
var option = jQuery(document.createElement("option"));
option.attr({ value: competition.WetId, title: competition.date_span });
option.text(competition.name);
if (competition.WetId == _data.WetId) option.attr("selected", true);
this.comp_chooser.append(option);
}
// fill category chooser
var option = jQuery(document.createElement("option"));
option.attr("value", "");
option.text(this.lang("Select a single category to show ..."));
this.cat_chooser.append(option);
var cat_found = false;
for (var i = 0; i < _data.categorys.length; ++i) {
var cat = _data.categorys[i];
option = jQuery(document.createElement("option"));
option.attr("value", cat.GrpId);
option.text(cat.name);
if (cat.GrpId == this.cat) {
option.attr("selected", true);
cat_found = true;
}
this.cat_chooser.append(option);
}
if (!cat_found) this.cat = undefined;
if (!this.widget) {
var widget_container = jQuery(document.createElement("div")).appendTo(
this.container
);
this.widget = new DrWidget(
widget_container,
this.json_url + (this.cat ? "&cat=" + this.cat : ""),
this.arg3
);
} else {
this.widget.navigateTo(
this.json_url + (this.cat ? "&cat=" + this.cat : "")
);
}
};
return ResultChooser;
})();
/**
* Dynamically load a css file
*
* @param href url to css file
*/
function load_css(href) {
//Get the head node and append a new link node with the stylesheet url to it
var headID = document.getElementsByTagName("head")[0];
var cssnode = document.createElement("link");
cssnode.type = "text/css";
cssnode.rel = "stylesheet";
cssnode.href = href;
headID.appendChild(cssnode);
}
/**
* Align non-breaking-space separated parts of text in a td by wrapping them in equally sized spans
*
* @param {string|jQuery} elems
*/
function align_td_nbsp(elems) {
jQuery(elems).replaceWith(function () {
var parts = jQuery(this)
.contents()
.text()
.split(/\u00A0+/);
if (parts.length == 1) return jQuery(this).clone();
var prefix = '<div class="tdAlign' + parts.length + '">';
var postfix = "</div>";
return jQuery(
"<td>" + prefix + parts.join(postfix + prefix) + postfix + "</td>"
).addClass(this.className);
});
}
/**
* Some compatibilty functions to cope with older javascript implementations
*/
if (!Array.isArray) {
Array.isArray = function (vArg) {
return Object.prototype.toString.call(vArg) === "[object Array]";
};
}
if (!Object.create) {
Object.create = function (o) {
function F() {}
F.prototype = o;
return new F();
};
}
if (!Date.getWeek) {
Date.prototype.getWeek = function () {
var onejan = new Date(this.getFullYear(), 0, 1);
return Math.ceil(((this - onejan) / 86400000 + onejan.getDay() + 1) / 7);
};
}