From cfd11b0d37132ece18dd677e6c4219b16c6bc455 Mon Sep 17 00:00:00 2001
From: CMDR furrycat
Date: Fri, 8 Jan 2016 09:31:31 -0500
Subject: [PATCH] Working race entries and standings.
---
entry.js | 778 ++++++++++++++++++++++++++
index.php | 29 +
lib/.htaccess | 2 +
lib/competitor.php | 48 ++
lib/elite_time.php | 47 ++
lib/endorsements.php | 30 +
lib/entry.php | 304 ++++++++++
lib/footer.php | 7 +
lib/forms.php | 88 +++
lib/header.php | 14 +
lib/hull.php | 42 ++
lib/include.php | 5 +
lib/lap.php | 51 ++
lib/race.php | 168 ++++++
lib/raikogram.php | 97 ++++
lib/ship.php | 80 +++
propel/.htaccess | 2 +
propel/build/classes/BuckyballObject.php | 33 ++
propel/build/classes/buckyball/Competitor.php | 21 +
propel/build/classes/buckyball/Entry.php | 21 +
propel/schema.xml | 74 ++-
race.js | 46 ++
style.css | 9 +
23 files changed, 1985 insertions(+), 11 deletions(-)
create mode 100644 entry.js
create mode 100644 index.php
create mode 100644 lib/.htaccess
create mode 100644 lib/competitor.php
create mode 100644 lib/elite_time.php
create mode 100644 lib/endorsements.php
create mode 100644 lib/entry.php
create mode 100644 lib/footer.php
create mode 100644 lib/forms.php
create mode 100644 lib/header.php
create mode 100644 lib/hull.php
create mode 100644 lib/include.php
create mode 100644 lib/lap.php
create mode 100644 lib/race.php
create mode 100644 lib/raikogram.php
create mode 100644 lib/ship.php
create mode 100644 propel/.htaccess
create mode 100644 propel/build/classes/BuckyballObject.php
create mode 100644 propel/build/classes/buckyball/Competitor.php
create mode 100644 propel/build/classes/buckyball/Entry.php
create mode 100644 race.js
create mode 100644 style.css
diff --git a/entry.js b/entry.js
new file mode 100644
index 0000000..6525624
--- /dev/null
+++ b/entry.js
@@ -0,0 +1,778 @@
+/* Get the table element. */
+function get_table() {
+ return document.getElementById('laps').getElementsByTagName('table')[0];
+}
+
+/* First time row in the table. */
+function first_row(table) {
+ /* First row is heading. */
+ return 1;
+}
+
+/* Last time row in the table. */
+function last_row(table) {
+ /* Last row is summary. */
+ for (var i = first_row(table); i < table.rows.length; i++) {
+ if (table.rows[i].cells[0].firstChild.tagName != 'P') return i - 1;
+ }
+ return table.rows.length - 2;
+}
+
+/* Row in the table with summaries. */
+function summary_row(table) {
+ return last_row(table) + 1;
+}
+
+/* Is this row a time row? */
+function valid_row(table, i, num_excluded) {
+ if (! num_excluded) num_excluded = 0;
+ /* First row is heading; last row is summary. */
+ return i >= first_row(table) && i <= last_row(table) - num_excluded;
+}
+
+/* First time column in the table. */
+function first_column(table) {
+ /* First column is station name; second is distance. */
+ return 2;
+}
+
+/* Last time column in the table. */
+function last_column(table) {
+ return table.rows[0].cells.length - 1;
+}
+
+/* Column in the table for leg distance. */
+function distance_column(table) {
+ return 1;
+}
+
+/* Is this column a time column? */
+function valid_column(table, j, num_excluded) {
+ if (! num_excluded) num_excluded = 0;
+ return j >= first_column(table) && j <= last_column(table) - num_excluded;
+}
+
+/* Does this element have a given class? */
+function has_class(element, c) {
+ for (var i = 0; i < element.classList.length; i++) {
+ if (element.classList[i] == c) return true;
+ }
+ return false;
+}
+
+/* Is this station the start line? */
+function start_line(table, i) {
+ return has_class(table.rows[i].cells[0], 'start_line');
+}
+
+/* Is this station the finish line? */
+function finish_line(table, i) {
+ return has_class(table.rows[i].cells[0], 'finish_line');
+}
+
+/* Get name of (station, lap) pair. */
+function get_cell_name(i, j) {
+ return 'lap' + String(j) + 'station' + String(i);
+}
+
+/* Get time by (station, lap) pair. */
+function get_cell_input(i, j) {
+ var table = get_table();
+ var row = i - first_row(table) + 1;
+ var column = j - first_column(table) + 1;
+ var id = get_cell_name(row, column);
+ var input = document.getElementById(id);
+ if (! input) console.log('Error: cell "' + id + '" not found in ' + Error().stack);
+ return input;
+}
+
+/* Extract an attribute encoded in element's class list. */
+function get_row_attribute_id(table, i, prefix) {
+ var regex = new RegExp('^' + prefix + '(\\d+)$');
+ var p = table.rows[i].cells[0].firstChild;
+ for (var j = 0; j < p.classList.length; j++) {
+ var m = p.classList[j].match(regex);
+ if (m) return parseInt(m[1]);
+ }
+ return null;
+}
+
+/* Get station ID of station. */
+function get_station_id(table, i) {
+ return get_row_attribute_id(table, i, 'station');
+}
+
+/* Get system ID of station; for distance calculation. */
+function get_system_id(table, i) {
+ return get_row_attribute_id(table, i, 'system');
+}
+
+/* Pad string. */
+function zero_pad(n) {
+ if (n < 10) return '0' + String(n);
+ else return String(n);
+}
+
+/* Convert a 21st century date to 34th century. */
+function elite_date(then) {
+ var now = new Date();
+ var year = String(now.getFullYear() + 1286);
+ var month = zero_pad(now.getMonth() + 1);
+ var day = zero_pad(now.getDate());
+ var date = (then) ? then : [year, month, day].join('-');
+ var hours = zero_pad(now.getHours());
+ var minutes = zero_pad(now.getMinutes());
+ var seconds = zero_pad(now.getSeconds());
+ var time = [hours, minutes, seconds].join(':');
+ return [date, time].join(' ');
+}
+
+/* Convert a 34th century date to 21st century. */
+function real_date(elite_date) {
+ var m = elite_date.match(/^(\d\d\d\d)(.*)/);
+ if (! m) return elite_date;
+ var elite_year = parseInt(m[1]);
+ if (elite_year < 3300) return elite_date;
+ return String(elite_year - 1286) + m[2];
+}
+
+/* Convert a date to timestamp. */
+function from_iso8601(date) {
+ var m = date.match(/^(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)$/);
+ if (! m) return 0;
+ return (new Date(parseInt(m[1]), parseInt(m[2]) - 1, parseInt(m[3]), parseInt(m[4]), parseInt(m[5]), parseInt(m[6])).getTime()) / 1000;
+}
+
+/* Convert a lap time to UNIX timestamp. */
+function parse_laptime(time) {
+ var m = time.match(/^(?:(\d+)\+)?(\d\d):(\d\d):(\d\d)$/);
+ if (! m) return 0;
+ var timestamp = parseInt(m[2]) * 3600 + parseInt(m[3]) * 60 + parseInt(m[4]);
+ if (m[1]) timestamp += parseInt(m[1]) * 86400;
+ return timestamp;
+}
+
+/* Format a time as HH:MM:SS. */
+function format_time(time) {
+ var formatted = '';
+
+ var days = Math.floor(time / 86400);
+ if (days) formatted += String(days) + '+';
+ time -= days * 86400;
+
+ var hours = Math.floor(time / 3600);
+ formatted += zero_pad(hours);
+ time -= hours * 3600;
+ formatted += ':';
+
+ var minutes = Math.floor(time / 60);
+ formatted += zero_pad(minutes);
+ time -= minutes * 60;
+ formatted += ':';
+
+ var seconds = Math.floor(time);
+ formatted += zero_pad(seconds);
+
+ return formatted;
+}
+
+/* Format the difference between two times. */
+function format_difference(time1, time2) {
+ if (! time1 || ! time2) return null;
+ var time = Math.max(time1, time2) - Math.min(time1, time2);
+ return format_time(time);
+}
+
+/* Calculate speed. */
+function speed(distance, time) {
+ /* Ly/h */
+ var speed = (distance / time) * 3600;
+ return speed;
+}
+
+/* Format a distance. */
+function format_distance(distance, suffix) {
+ if (suffix !== false) suffix = 'Ly';
+ return String(distance.toFixed(2)) + suffix;
+}
+
+/* Format a speed. */
+function format_speed(speed, suffix) {
+ if (suffix !== false) suffix = '/h';
+ return format_distance(speed) + suffix;
+}
+
+/* Calculate bonus time. */
+function calculate_bonus(laps) {
+ var time = 0;
+ var hull_start = 0;
+ var hull_end = 0;
+ var last_hull = 100;
+ var repaired = false;
+ for (var lap = 1; lap <= laps; lap++) {
+ var input = document.getElementById('lap' + String(lap) + 'hullstart');
+ if (input.value.match(input.pattern)) {
+ hull_start = parseInt(input.value);
+ if (hull_start > last_hull) repaired = true;
+ }
+ input = document.getElementById('lap' + String(lap) + 'hullend');
+ if (input.value.match(input.pattern)) {
+ hull_end = parseInt(input.value);
+ if (hull_end > last_hull) repaired = true;
+ last_hull = hull_end;
+ if (hull_end > hull_start) repaired = true;
+ else time += Math.floor(hull_end / 2);
+ }
+ }
+ /* Double bonus if no repairs at all. */
+ if (! repaired) time *= 2;
+ return time;
+}
+
+/* Calculate penalty time. */
+function calculate_penalty(allotted_time, bonus_time, raced_time) {
+ if (! allotted_time) return 0;
+ var time = allotted_time + bonus_time;
+ if (raced_time < time) {
+ /* Cut short. */
+ return time - raced_time;
+ }
+ else return raced_time - time;
+}
+
+/* Calculate total time. */
+function calculate_total(allotted_time, bonus_time, penalty_time, raced_time) {
+ if (raced_time > allotted_time) return allotted_time + bonus_time + penalty_time;
+ /* Cut short. */
+ return allotted_time + bonus_time;
+}
+
+/*
+ Update placeholder time based on current date, so subsequent entries can be added quickly.
+*/
+function propagate_placeholder(table, date) {
+ for (var i = first_row(table); valid_row(table, i); i++) {
+ for (var j = first_column(table); valid_column(table, j); j++) {
+ var input = get_cell_input(i, j);
+ if (input) input.placeholder = elite_date(date);
+ }
+ }
+}
+
+/* Update point-to-point distance array. */
+function update_raikogram(table) {
+ var n = 1;
+ var lap_distance = 0.0;
+ for (var i = first_row(table); valid_row(table, i); i++) {
+ var cell = table.rows[i].cells[distance_column(table)];
+ cell.id = 'distance' + String(n);
+ if (n++ < 2) continue;
+ var first = get_system_id(table, i - 1);
+ var second = get_system_id(table, i);
+ if (! first || ! second) continue;
+ var system1 = Math.min(first, second);
+ var system2 = Math.max(first, second);
+ var distance = distances['from' + system1 + 'to' + system2];
+ if (! distance) continue;
+ lap_distance += distance;
+ cell.firstChild.innerHTML = format_distance(distance);
+ }
+ /* Total. */
+ var text = (lap_distance > 0) ? format_distance(lap_distance) : '';
+ table.rows[summary_row(table)].cells[distance_column(table)].firstChild.innerHTML = text;
+}
+
+/* Exchange cells. */
+function swap_cells(table, i1, j1, i2, j2, deleting) {
+ var row1 = table.rows[i1];
+ var row2 = table.rows[i2];
+ row1.cells[j1].insertBefore(row2.cells[j2].firstChild, row1.cells[j1].firstChild);
+ row2.cells[j2].appendChild(row1.cells[j1].children[1]);
+ set_ids(table, Math.min(j1, j2));
+ if (deleting) return;
+ /* Fire update hook unless we're deleting the second cell. */
+ changed_cell(table, i1, j1);
+ changed_cell(table, i2, j2);
+}
+
+/* Reorder station visit so times are sequential across laps. */
+function reorder_row(table, i) {
+ var changed = false;
+ for (var j = first_column(table); valid_column(table, j, 1); j++) {
+ var left = get_cell_input(i, j).value || 'zzz';
+ var right = get_cell_input(i, j + 1).value || 'zzz';
+ if (left == right) continue;
+ if (left < right) continue;
+ swap_cells(table, i, j, i, j + 1);
+ changed = true;
+ j = 0;
+ }
+
+ return changed;
+}
+
+/* Reorder a lap so station visit times are sequential. */
+function reorder_column(table, j) {
+ var changed = false;
+ for (var i = first_row(table); valid_row(table, i, 1); i++) {
+ if (start_line(table, i) || finish_line(table, i)) continue;
+ if (start_line(table, i + 1) || finish_line(table, i + 1)) continue;
+ var above = get_cell_input(i, j).value || 'zzz';
+ var below = get_cell_input(i + 1, j).value || 'zzz';
+ if (above == below) continue;
+ if (above < below) continue;
+ for (var k = 0; k < table.rows[i].cells.length; k++) swap_cells(table, i, k, i + 1, k, true)
+ changed = true;
+ i = first_row(table);
+ }
+ if (changed) update_raikogram(table);
+ return changed;
+}
+
+/* Update cell IDs after row or column reordering. */
+function set_ids(table, j) {
+ j = Math.max(j, first_column(table));
+ for (var i = first_row(table); valid_row(table, i); i++) {
+ /* Don't use valid_column() because we may be removing a column. */
+ for (var k = j; k < table.rows[i].cells.length; k++) {
+ var input = table.rows[i].cells[k].firstChild;
+ var row = i - first_row(table) + 1;
+ var column = k - first_column(table) + 1;
+ input.id = input.name = get_cell_name(row, column);
+ }
+ }
+ var row = table.rows[last_row(table) + 1];
+ for (var k = j; k < row.cells.length; k++) {
+ var lap = k - first_column(table) + 1;
+ var p = row.cells[k].firstChild;
+ var summary = p.getElementsByTagName('span')[0];
+ summary.id = summary.name = 'lap' + String(lap) + 'summary';
+ var hull = p.getElementsByTagName('input')[0];
+ hull.id = hull.name = 'lap' + String(lap) + 'hullstart';
+ hull = p.getElementsByTagName('input')[1];
+ hull.id = hull.name = 'lap' + String(lap) + 'hullend';
+ }
+}
+
+/* Update hidden field and visible field. */
+function do_stat(id, value, visible_value) {
+ if (typeof value == 'undefined') value = '';
+ if (typeof visible_value == 'undefined') visible_value = String(value);
+ console.log([id, visible_value].join(': '));
+ document.getElementsByName('stat_' + id)[0].value = value;
+ document.getElementById(id).innerHTML = visible_value;
+}
+
+/* Update entry stats. */
+function do_stats(table) {
+ var last_lap = first_column(table);
+ var last_station = last_row(table);
+ var complete_laps = 0;
+ /* Find the last lap and last station visited. */
+ for (var j = first_column(table); valid_column(table, j, 1); j++) {
+ for (var i = last_row(table); valid_row(table, i); i--) {
+ var input = get_cell_input(i, j);
+ if (input.value.match(input.pattern)) {
+ last_lap = j;
+ if (i == last_row(table)) complete_laps++;
+ break;
+ }
+ }
+ }
+ for (var i = last_station; valid_row(table, i); i--) {
+ var input = get_cell_input(i, last_lap);
+ if (input.value.match(input.pattern)) {
+ last_station = i;
+ break;
+ }
+ }
+ /* Find total distance. */
+ var lap_distance = parseFloat(table.rows[summary_row(table)].cells[distance_column(table)].firstChild.innerHTML.replace(/Ly$/, ''));
+ var total_distance = lap_distance * complete_laps;
+ var input = get_cell_input(last_row(table), last_lap);
+ if (! input.value.match(input.placeholder)) {
+ /* Last lap incomplete. */
+ for (var i = last_station; valid_row(table, i, 1); i--) {
+ var cell = document.getElementById('distance' + String(i - first_row(table) + 1));
+ var text = cell.firstChild.innerHTML.replace(/Ly$/, '');
+ if (! text) continue;
+ var distance = parseFloat(text);
+ total_distance += distance;
+ }
+ }
+
+ var start_time = from_iso8601(real_date(get_cell_input(first_row(table), first_column(table)).value));
+ var finish_time = from_iso8601(real_date(get_cell_input(last_station, last_lap).value));
+ var raced_time = finish_time - start_time;
+ var bonus_time = calculate_bonus(complete_laps);
+ var penalty_time = calculate_penalty(allotted_time, bonus_time, raced_time);
+ var total_time = calculate_total(allotted_time, bonus_time, penalty_time, raced_time);
+
+ /* Lap distance. */
+ do_stat('lap_distance', lap_distance, String(lap_distance) + 'Ly');
+ /* Complete laps. */
+ do_stat('complete_laps', complete_laps);
+ /* Total distance. */
+ do_stat('total_distance', format_distance(total_distance, false), total_distance ? format_distance(total_distance) : '');
+ /* Times. */
+ do_stat('allotted_time', allotted_time, format_time(allotted_time));
+ do_stat('bonus_time', bonus_time, format_time(bonus_time));
+ do_stat('penalty_time', penalty_time, format_time(penalty_time));
+ do_stat('raced_time', finish_time - start_time, format_difference(finish_time, start_time));
+ do_stat('total_time', total_time, format_time(total_time));
+ if (complete_laps) {
+ /* Fastest lap. */
+ var fastest_lap = null;
+ for (var j = 1; j <= complete_laps; j++) {
+ var laptime = parse_laptime(document.getElementById('lap' + String(j) + 'summary').innerHTML);
+ if (fastest_lap == null || fastest_lap > laptime) fastest_lap = laptime;
+ }
+ do_stat('fastest_lap', fastest_lap, format_time(fastest_lap));
+ /* Average lap. */
+ var average_lap = Math.floor(total_time / complete_laps);
+ do_stat('average_lap', average_lap, format_time(average_lap));
+ /* Best speed. */
+ var best_speed = speed(lap_distance, fastest_lap);
+ do_stat('best_speed', format_speed(best_speed, false), format_speed(best_speed));
+ /* Average speed. */
+ var average_speed = speed(total_distance, total_time);
+ do_stat('average_speed', format_speed(average_speed, false), format_speed(average_speed));
+ }
+ else {
+ do_stat('fastest_lap');
+ do_stat('average_lap');
+ do_stat('best_speed');
+ do_stat('average_speed');
+ }
+}
+
+/* Check entry validity. */
+function validate(table, cloned) {
+ var valid = true;
+ for (var i = first_row(table); valid_row(table, i); i++) {
+ for (var j = first_column(table); valid_column(table, j); j++) {
+ var input = get_cell_input(i, j);
+ if (input) input.setCustomValidity("");
+ }
+ }
+
+ var rows = last_row(table) - first_row(table) + 1;
+ var columns = table.rows[0].cells.length;
+ var last_time = 0;
+ for (var j = first_column(table); valid_column(table, j); j++) {
+//for (var c = first_column(table); valid_column(table, c); c++) {
+// var j = c - first_column(table) + 1;
+ var lap = j - first_column(table) + 1;
+ var first_leg_time = 0;
+ var last_leg_time = 0;
+ var summary = document.getElementById('lap' + String(lap) + 'summary');
+ summary.innerHTML = null;
+ var no_stations_visited = true;
+ var all_stations_visited = true;
+ /* Ensure station times are sequential. */
+ for (var i = first_row(table); valid_row(table, i); i++) {
+ var input = get_cell_input(i, j);
+ var text = input.value;
+ if (text.match(input.pattern)) {
+ input.setCustomValidity("");
+ no_stations_visited = false;
+ var time = from_iso8601(real_date(text));
+ if (i == first_row(table)) first_leg_time = time;
+ else last_leg_time = time;
+ if (time <= last_time) {
+ if (j == first_column(table) && i > first_row(table) && i < last_row(table)) {
+ /* Revalidate if lap was reordered. */
+ if (reorder_column(table, j)) return validate(table);
+ }
+ if (time < last_time || i > first_row(table)) {
+ input.setCustomValidity('Times out of sequence');
+ valid = false;
+ }
+ }
+ last_time = time;
+ }
+ else all_stations_visited = false;
+ }
+ if (! valid) break;
+ if (j >= first_column(table) && j < last_column(table) - 1) {
+ /* Intermediate laps must be complete. */
+ if (! all_stations_visited) {
+ valid = false;
+ for (var i = first_row(table); valid_row(table, i); i++) {
+ var input = get_cell_input(i, j);
+ if (input.value.match(input.pattern)) continue;
+ input.setCustomValidity('Lap ' + String(lap) + ' incomplete');
+ break;
+ }
+ }
+ }
+ else {
+ if (j == first_column(table)) {
+ /* First lap must have stations. */
+ if (no_stations_visited) {
+ valid = false;
+ var input = get_cell_input(1, j);
+ input.setCustomValidity('Race not started');
+ }
+ }
+ }
+ if (! valid) break;
+ if (! no_stations_visited) {
+ /* Incomplete laps are valid if visited stations are in order. */
+ var last = last_row(table) + 1;
+ for (var i = last_row(table); valid_row(table, i); i--) {
+ var input = get_cell_input(i, j);
+ if (! input.value.match(input.pattern)) continue;
+ last = i;
+ break;
+ }
+ for (var i = first_row(table); i < last; i++) {
+ var input = get_cell_input(i, j);
+ if (input.value.match(input.pattern)) continue;
+ valid = false;
+ /* First lap (only) can be reordered. */
+ if (j == first_column(table) && i > first_row(table) && i < last_row(table)) {
+ /* Revalidate if lap was reordered. */
+ if (reorder_column(table, j)) return validate(table);
+ }
+ input.setCustomValidity('Final lap ' + String(lap) + ' incomplete');
+ break;
+ }
+ }
+ if (! valid) break;
+ summary.innerHTML = format_difference(last_leg_time, first_leg_time);
+ if (! no_stations_visited && ! summary.innerHTML) summary.innerHTML = '--:--:--';
+ }
+
+ if (valid) {
+ /* Remember station ordering. */
+ var station_orders = document.getElementsByName('station_order');
+ if (station_orders.length) {
+ var stations = [];
+ for (var i = first_row(table); valid_row(table, i); i++) {
+ var station_id = get_station_id(table, i);
+ if (station_id) stations.push(station_id);
+ }
+ station_orders[0].value = JSON.stringify(stations);
+ }
+ do_stats(table);
+ }
+ else {
+ /* Revalidate if row is reordered. */
+ for (var i = first_row(table); valid_row(table, i); i++) {
+ if (i == cloned) continue;
+ if (reorder_row(table, i)) return validate(table);
+ }
+ }
+
+ var submits = document.getElementsByName('add_entry');
+ if (submits.length) submits[0].disabled = ! valid;
+}
+
+/* A cell was changed. */
+function changed_cell(table, row, column, input) {
+ if (! input) input = get_cell_input(row, column);
+ var text = input.value;
+ if (text && text.length < input.placeholder.length) {
+ /* Pad remainder with placeholder. */
+ var placeheld = input.placeholder.substr(0, input.placeholder.length - text.length) + text;
+ if (placeheld.match(input.pattern)) input.value = text = placeheld;
+ }
+ var m = text.match(input.pattern);
+ if (m) {
+ var date = m[1];
+ /* Add a lap if this was the last column. */
+ if (column == last_column(table) - first_column(table) + 1) add_lap(table, m[1]);
+ }
+ var cloned = null;
+ if (m || ! text) {
+ /*
+ Docking time of previous lap and launch time of next lap default to
+ being the same.
+ */
+ if (row == 1 && column > 1 && column < last_column(table) - first_column(table)) {
+ /* Set docking time to launch time. */
+ cloned = last_row(table);
+ var replaced = table.rows[last_row(table)].cells[first_column(table) + column - 2].firstChild;
+ if (! replaced.value != ! text) replaced.value = text;
+ }
+ else if (row == last_row(table) - first_row(table) + 1) {
+ /* Set launch time to docking time. */
+ cloned = first_row(table);
+ var replaced = table.rows[first_row(table)].cells[first_column(table) + column].firstChild;
+ if (! replaced.value && text) replaced.value = text;
+ }
+ }
+ delete_empty_laps(table);
+ validate(table, cloned);
+}
+
+/* Callback when an input element was changed. */
+function changed(event) {
+ var input = event.target;
+ var m = input.id.match(/^lap(\d+)station(\d+)$/);
+ if (! m) return;
+ var table = get_table();
+ return changed_cell(table, parseInt(m[2]), parseInt(m[1]), input);
+}
+
+/* A hull percentage was changed. */
+function changed_hull_percentage(table, lap, suffix, input) {
+ var text = input.value;
+ var m = text.match(input.pattern);
+ if (text) {
+ if (! m) return;
+ if (suffix == 'end') {
+ var next_lap = lap + 1;
+ var next_input = document.getElementById('lap' + String(next_lap) + 'hullstart');
+ /* Set hull at start of next lap to value at end of this. */
+ if (next_input && ! next_input.value) next_input.value = text;
+ }
+ }
+ return validate(table);
+}
+
+/* Callback when a hull percentage was changed. */
+function changed_hull(event) {
+ var input = event.target;
+ var m = input.id.match(/^lap(\d+)hull(start|end)$/);
+ if (! m) return;
+ var table = get_table();
+ return changed_hull_percentage(table, parseInt(m[1]), m[2], input);
+}
+
+/* Create a new input element. */
+function checkpoint(id) {
+ var date = elite_date();
+ var input = document.createElement('input');
+ input.autocomplete = 'off';
+ input.type = 'text';
+ input.pattern = '^(33[0-9][0-9]-(?:0[1-9]|1[0-2])-(?:[0-2][0-9]|3[01])) ((?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9])$';
+ input.maxLength = input.size = 19;
+ input.id = input.name = id;
+ input.tabIndex = parseInt(id.replace(/^lap(\d+)station\d+$/, function(ignored, ret) { return ret; }));
+ input.onchange = changed;
+ return input;
+}
+
+/* Create a hull percentage element. */
+function hull(lap, suffix) {
+ var input = document.createElement('input');
+ input.autocomplete = 'off';
+ input.type = 'text';
+ input.placeholder = 'Hull';
+ input.pattern = '^(?:0|100|[1-9][0-9]?)$';
+ input.maxLength = input.size = 3;
+ input.id = input.name = 'lap' + String(lap) + 'hull' + suffix;
+ input.onchange = changed_hull;
+ return input;
+}
+
+/* Create an element to display lap summary. */
+function summary(lap) {
+ var span = document.createElement('span');
+ span.id = 'lap' + String(lap) + 'summary';
+ span.style.fontFamily = 'monospace';
+ return span;
+}
+
+/* Add a new empty lap. */
+function add_lap(table, date) {
+ var lap = table.rows[0].cells.length - first_column(table) + 1;
+ var th = document.createElement('th');
+ th.appendChild(document.createTextNode('Lap ' + String(lap)));
+ table.rows[0].appendChild(th);
+ for (var i = first_row(table); valid_row(table, i); i++) {
+ var td = document.createElement('td');
+ td.appendChild(checkpoint(get_cell_name(i, lap)));
+ table.rows[i].appendChild(td);
+ }
+ var td = document.createElement('td');
+ var p = document.createElement('p');
+ p.appendChild(hull(lap, 'start'));
+ p.appendChild(hull(lap, 'end'));
+ p.appendChild(summary(lap));
+ td.appendChild(p);
+ table.rows[last_row(table) + 1].appendChild(td);
+ propagate_placeholder(table, date);
+}
+
+/* Delete empty laps other than the final one. */
+function delete_empty_laps(table) {
+ var columns = table.rows[0].cells.length;
+ for (var j = last_column(table) - 1; valid_column(table, j); j--) {
+ var empty = true;
+ for (var i = first_row(table); valid_row(table, i); i++) {
+ var input = get_cell_input(i, j);
+ if (! input) continue;
+ if (input.value.match(input.pattern)) {
+ empty = false;
+ break;
+ }
+ }
+ if (! empty) continue;
+ /* Include summary. */
+ for (var i = first_row(table); i <= summary_row(table); i++) {
+ /* Shift subsequent laps back. */
+ var row = table.rows[i];
+ swap_cells(table, i, j, i, j + 1, true);
+ row.removeChild(row.cells[j + 1]);
+ }
+ /* Remove header. */
+ table.rows[0].removeChild(table.rows[0].cells[columns - 1]);
+ /* Remove summary. */
+ //table.rows[table.rows.length - 1].removeChild(table.rows[table.rows.length - 1].cells[columns - 1]);
+ //table.rows[summary_row(table)].removeChild(table.rows[summary_row(table)].cells[columns - 1]);
+ }
+}
+
+/* Document onload. */
+function loaded() {
+ var table = get_table();
+ add_lap(table);
+ if (typeof posted != 'undefined') {
+ /* Times were submitted. */
+ var station_order = JSON.parse(posted['station_order']);
+ if (station_order) {
+ /* Stations were rearranged. */
+ for (var i = station_order.length - 1; i >= 0; i--) {
+ row = i + 1;
+ var actual = get_station_id(table, row);
+ if (actual == station_order[i]) continue;
+ for (var other_row = first_row(table); valid_row(table, other_row); other_row++) {
+ var other = get_station_id(table, other_row);
+ if (other != station_order[i]) continue;
+ for (var k = 0; k < table.rows[row].cells.length; k++) swap_cells(table, row, k, other_row, k, true)
+ i = station_order.length - 1;
+ break;
+ }
+ }
+ set_ids(table, first_column(table));
+ }
+ Object.keys(posted).forEach(function(id, index) {
+ if (! id) return;
+ if (! posted[id]) return;
+ var m = id.match(/^lap(\d+)station(\d+)$/);
+ if (m) {
+ /* Insert times. */
+ var input = document.getElementById(id);
+ input.value = posted[id];
+ changed_cell(table, parseInt(m[2]), parseInt(m[1]), input);
+ return;
+ }
+ m = id.match(/^lap(\d+)hull(?:start|end)/);
+ if (m) {
+ /* Insert hull. */
+ var input = document.getElementById(id);
+ input.value = posted[id];
+ return;
+ }
+ });
+ }
+ if (typeof distances != 'undefined') update_raikogram(table);
+ validate(table);
+ var stats = document.getElementById('stats').getElementsByTagName('table')[0];
+ stats.rows[0].cells[0].style.width = String(table.rows[0].cells[0].offsetWidth) + 'px';
+}
+
+document.onload = loaded();
diff --git a/index.php b/index.php
new file mode 100644
index 0000000..24ef45c
--- /dev/null
+++ b/index.php
@@ -0,0 +1,29 @@
+What's all that about, then?
\n";
+ }
+ include_once(join(DIRECTORY_SEPARATOR, array($lib_root, "footer.php")));
+?>
diff --git a/lib/.htaccess b/lib/.htaccess
new file mode 100644
index 0000000..93169e4
--- /dev/null
+++ b/lib/.htaccess
@@ -0,0 +1,2 @@
+Order deny,allow
+Deny from all
diff --git a/lib/competitor.php b/lib/competitor.php
new file mode 100644
index 0000000..53aa136
--- /dev/null
+++ b/lib/competitor.php
@@ -0,0 +1,48 @@
+Competitors
+Missing CMDR name!\n";
+ return false;
+ }
+ $competitor = new Competitor;
+ $competitor->setCmdrName($cmdr_name);
+ if ($forum_name && $forum_name != $cmdr_name) $competitor->setForumName($forum_name);
+ try {
+ $competitor->save();
+ return true;
+ }
+ catch (Exception $e) {
+ echo "Error adding competitor: " . $e->getMessage() . "
\n";
+ }
+ return false;
+ }
+
+ function module_competitor($action) {
+ if ($_POST['add_competitor']) {
+ if (add_competitor($_POST["cmdr_name"], $_POST["forum_name"])) unset($_POST);
+ }
+
+ $q = new CompetitorQuery;
+ $competitors = $q->orderByCmdrName()->find();
+ if (! count($competitors)) echo "No competitors
\n";
+ foreach ($competitors as $competitor) {
+ $cmdr = $competitor->getCmdrName();
+ echo "CMDR $cmdr";
+ $forum = $competitor->getForumName();
+ if ($forum && $forum != $cmdr) echo " ($forum)";
+ "
\n";
+ }
+
+ echo "
\n";
+ form();
+ echo "Add a new competitor: CMDR ";
+ input("cmdr_name", $_POST['cmdr_name']);
+ echo " forum: ";
+ input("forum_name", $_POST['forum_name']);
+ submit("add_competitor", "Add");
+ echo "
\n";
+ end_form();
+ }
+?>
diff --git a/lib/elite_time.php b/lib/elite_time.php
new file mode 100644
index 0000000..9930334
--- /dev/null
+++ b/lib/elite_time.php
@@ -0,0 +1,47 @@
+= 3300) return $time;
+ return sprintf("%04d%s", $year + 1286, $remainder);
+ }
+
+ function real_time($time) {
+ if (! preg_match('/^(\d\d\d\d)(.+)$/', $time, $m)) return null;
+ list($ignored, $year, $remainder) = $m;
+ if ($year < 3300) return $time;
+ return sprintf("%04d%s", $year - 1286, $remainder);
+ }
+
+ function format_time($time) {
+ if (! preg_match('/^\d+$/', $time)) return "";
+
+ $ret = "";
+ $days = floor($time / 86400);
+ if ($days) {
+ $ret .= sprintf("%d+", $days);
+ $time -= $days * 86400;
+ }
+
+ $hours = floor($time / 3600);
+ $time -= $hours * 3600;
+
+ $minutes = floor($time / 60);
+ $time -= $minutes * 60;
+
+ $seconds = $time;
+ $ret .= sprintf("%02d:%02d:%02d", $hours, $minutes, $seconds);
+
+ return $ret;
+ }
+
+ function format_distance($distance) {
+ return round($distance, 2);
+ }
+
+ function format_speed($speed) {
+ return round($speed, 2);
+ }
+
+?>
diff --git a/lib/endorsements.php b/lib/endorsements.php
new file mode 100644
index 0000000..b09c973
--- /dev/null
+++ b/lib/endorsements.php
@@ -0,0 +1,30 @@
+ array('entry cut short', 1 << 0),
+ 'd' => array('docking computer', 1 << 1),
+ 'j' => array('joker used', 1 << 2),
+ 'x' => array('excluded from ranking', 1 << 3),
+ );
+
+ function endorsement_keys() {
+ return array_keys($GLOBALS['ENDORSEMENTS']);
+ }
+
+ function endorsement($e, $endorsements = ~0) {
+ return $endorsements & $GLOBALS['ENDORSEMENTS'][$e][1];
+ }
+
+ function endorsement_description($e) {
+ return $GLOBALS['ENDORSEMENTS'][$e][0];
+ }
+
+ function endorsement_string($endorsements) {
+ $ret = "";
+ foreach (endorsement_keys() as $e) {
+ if (endorsement($e, $endorsements)) $ret .= $e;
+ }
+ return $ret;
+ }
+
+?>
diff --git a/lib/entry.php b/lib/entry.php
new file mode 100644
index 0000000..7d212b4
--- /dev/null
+++ b/lib/entry.php
@@ -0,0 +1,304 @@
+Entries
+getRace()->getAllottedTime();
+ foreach ($entry->getLaps() as $lap) {
+ $lap_number = $lap->getLapNumber();
+ if ($allotted_time && $lap->getHullStart() && $lap->getHullEnd()) {
+ $_POST["lap${lap_number}hullstart"] = $lap->getHullStart();
+ $_POST["lap${lap_number}hullend"] = $lap->getHullEnd();
+ }
+ foreach ($lap->getLaptimes() as $laptime) {
+ $station_id = $laptime->getStationId();
+ $station_number = $laptime->getStationOrder();
+ if ($lap_number == 1) $station_order[] = $station_id;
+ $_POST["lap${lap_number}station${station_number}"] = elite_time($laptime->getArrival());
+ }
+ }
+ foreach (endorsement_keys() as $e) {
+ if (endorsement($e, $entry->getEndorsements())) $_POST["endorsement_$e"] = true;
+ }
+ $_POST['station_order'] = json_encode($station_order);
+ }
+
+ function do_entry($race_id, $ship_id, $posted, $allotted_time) {
+ $dbh = Propel::getConnection(LapPeer::DATABASE_NAME);
+ $dbh->beginTransaction();
+
+ $station_order = json_decode($posted['station_order']);
+
+ $endorsements = 0;
+ foreach (array_keys($GLOBALS['ENDORSEMENTS']) as $e) {
+ if ($posted["endorsement_$e"]) $endorsements |= endorsement($e);
+ }
+
+ $entry = new Entry;
+ $entry->setRaceId($race_id);
+ $entry->setShipId($ship_id);
+ $entry->setLapDistance($posted['stat_lap_distance']);
+ $entry->setCompleteLaps($posted['stat_complete_laps']);
+ $entry->setTotalDistance($posted['stat_total_distance']);
+ if ($allotted_time > 0) {
+ $entry->setPenaltyTime($posted['stat_penalty_time']);
+ $entry->setBonusTime($posted['stat_bonus_time']);
+ if ($posted['stat_raced_time'] < $allotted_time) $endorsements |= endorsement('c');
+ if ($posted['stat_bonus_time']) $endorsements |= endorsement('j');
+ }
+ $entry->setRacedTime($posted['stat_raced_time']);
+ $entry->setTotalTime($posted['stat_total_time']);
+ $entry->setFastestLap($posted['stat_fastest_lap']);
+ $entry->setAverageLap($posted['stat_average_lap']);
+ $entry->setBestSpeed($posted['stat_best_speed']);
+ $entry->setAverageSpeed($posted['stat_average_speed']);
+ $entry->setEndorsements($endorsements);
+ $entry->save();
+
+ $lap_number = 1;
+ while (array_key_exists("lap${lap_number}station2", $posted)) {
+ $lap = new Lap;
+ $lap->setEntryId($entry->getId());
+ $lap->setLapNumber($lap_number);
+ if ($posted["lap${lap_number}hullstart"] && $posted["lap${lap_number}hullend"]) {
+ $lap->setHullStart($posted["lap${lap_number}hullstart"]);
+ $lap->setHullEnd($posted["lap${lap_number}hullend"]);
+ }
+ $lap->save();
+ $station_number = 1;
+ while (array_key_exists("lap${lap_number}station${station_number}", $posted)) {
+ $station_id = $station_order[$station_number - 1];
+ $arrival = real_time($posted["lap${lap_number}station${station_number}"]);
+ $laptime = new Laptime;
+ $laptime->setLapId($lap->getId());
+ $laptime->setStationOrder($station_number);
+ $laptime->setStationId($station_id);
+ $laptime->setArrival($arrival);
+ $laptime->save();
+ $station_number++;
+ }
+ $lap_number++;
+ }
+
+ $dbh->commit();
+
+ return true;
+ }
+
+ function add_entry($race_id, $ship_id, $entry) {
+ $q = new CourseQuery;
+ $q->joinWith("Course.Station");
+ $q->joinWith("Course.Race");
+ $q->joinWith("Station.System");
+ $q->findByRaceId($race_id);
+ $course = $q->find();
+ $allotted_time = 0 + $course[0]->getRace()->getAllottedTime();
+ $legs = array();
+ $system_ids = array();
+ $station_order = array();
+ $system_order = array();
+ foreach ($course as $leg) {
+ $station = $leg->getStation();
+ $station_name = $station->getName();
+ $system = $station->getSystem();
+ $system_id = $system->getId();
+ $station_order[] = $system_id;
+ $system_ids[] = $system_id;
+ $system_order[] = $system_id;
+ $flags = 0;
+ if ($leg->getStartLine()) $flags |= 2;
+ else if (! $leg->getFinishLine()) $flags |= 1;
+ $legs[$station_name] = array($flags, $station->getId(), $system_id);
+ }
+ arsort($legs);
+ $system_ids = array_unique($system_ids);
+ $rq = new RaikogramQuery;
+ $rq->filterBySystem1($system_ids);
+ $rq->filterBySystem2($system_ids);
+ $raikograms = $rq->find();
+ $distances = array();
+ foreach ($raikograms as $raikogram) {
+ $system1 = $raikogram->getSystem1();
+ $system2 = $raikogram->getSystem2();
+ $distance = $raikogram->getDistance();
+ $distances["from${system1}to${system2}"] = $distance;
+ }
+ echo "\n";
+ if ($entry) {
+ $entry_id = $entry->getId();
+ $ship_name = $entry->getShip()->getName();
+ $cmdr_name = $entry->getShip()->getCompetitor()->getCmdrName();
+ echo "Entry $entry_id: CMDR $cmdr_name flying $ship_name
\n";
+ }
+ else if (do_entry($race_id, $ship_id, $posted, $allotted_time)) return true;
+ }
+ else echo "//-->\n";
+ if (! $entry) {
+ $sq = new ShipQuery;
+ $sq->joinWith('Ship.Competitor');
+ $ship = $sq->findOneById($ship_id);
+ $cmdr_name = $ship->getCompetitor()->getCmdrName();
+ $ship_name = $ship->getName();
+ echo "New entry: CMDR $cmdr_name flying $ship_name
\n";
+ }
+ form();
+ echo "\n";
+ echo "
\n";
+ echo "| Station | \n";
+ echo "Leg | \n";
+ $n = 0;
+ foreach ($legs as $station => $attributes) {
+ list($flags, $id, $system_id) = $attributes;
+ $n++;
+ $label = $station;
+ $class = ($flags & 2) ? ' class="start_line"' : '';
+ echo "\n";
+ echo "$station | \n";
+ echo " | \n";
+ echo "
\n";
+ }
+ foreach ($legs as $station => $attributes) {
+ list($flags, $id, $system_id) = $attributes;
+ if ($flags & 1) continue;
+ $class = ' class="finish_line"';
+ echo "\n";
+ echo "$station | \n";
+ echo " | \n";
+ echo "
\n";
+ }
+ echo "\n";
+ echo "| ";
+ hidden('race_id', $race_id);
+ hidden('ship_id', $ship_id);
+ hidden('station_order', $station_order);
+ $stats = array('Lap distance', 'Complete laps', 'Total distance', 'Allotted time', 'Bonus time', 'Raced time', 'Penalty time', 'Total time', 'Fastest lap', 'Average lap', 'Best speed', 'Average speed');
+ foreach ($stats as $stat) {
+ $id = preg_replace('/[^a-z0-9]/', '_', strtolower($stat));
+ hidden("stat_$id");
+ }
+ if ($entry) hidden("add_entry");
+ else submit("add_entry", "Submit entry");
+ echo " | ";
+ echo "total distance | \n"; /* Distance column. */
+ echo "
\n";
+ echo "
\n";
+ echo "
";
+ foreach (array('d', 'x') as $e) {
+ $name = "endorsement_$e";
+ $description = ucfirst(endorsement_description($e));
+ echo "$description: \n";
+ }
+ echo "
\n";
+ end_form();
+ echo "
\n";
+ echo "\n";
+ echo "
\n";
+ foreach ($stats as $stat) {
+ $id = preg_replace('/[^a-z0-9]/', '_', strtolower($stat));
+ echo "\n";
+ echo "| $stat | \n";
+ echo " | \n";
+ echo "
\n";
+ }
+ echo "
\n";
+ echo "
\n";
+ return false;
+ }
+
+ function module_entry($action) {
+ if ($action && ! count($_POST)) {
+ $eq = new EntryQuery;
+ $eq->joinWith('Entry.Race');
+ $eq->joinWith('Entry.Ship');
+ $eq->joinWith('Ship.Competitor');
+ $eq->joinWith('Entry.Lap');
+ $eq->joinWith('Lap.Laptime');
+ $entry = $eq->findOneById($action);
+ if ($entry) add_entry($entry->getRace()->getId(), $entry->getShip()->getId(), $entry);
+ }
+ else if ($_POST['add_entry']) {
+ if (add_entry($_POST["race_id"], $_POST["ship_id"])) unset($_POST);
+ }
+
+ $rq = new RaceQuery;
+ $races = $rq->find();
+ if (! count($races)) {
+ echo "Add races before submitting entries.
\n";
+ return;
+ }
+ $sq = new ShipQuery;
+ $sq->joinWith("Ship.Competitor");
+ $ships = $sq->orderBy('Competitor.CmdrName')->orderByName()->find();
+ if (! count($ships)) {
+ echo "Add ships before submitting entries.
\n";
+ return;
+ }
+ if (! $_POST['add_entry']) {
+ $eq = new EntryQuery;
+ $eq->joinWith('Entry.Race');
+ $eq->joinWith('Entry.Ship');
+ $eq->joinWith('Ship.Competitor');
+ $entries = $eq->orderBy('Race.Id')->orderBy('Competitor.CmdrName')->orderBy('Ship.Name')->orderById()->find();
+ foreach ($entries as $entry) {
+ $entry_link = $entry->getLink();
+ $race_name = $entry->getRace()->getLink();
+ $ship_name = $entry->getShip()->getName();
+ $cmdr_name = $entry->getShip()->getCompetitor()->getCmdrName();
+ echo "CMDR $cmdr_name flying $ship_name in $race_name [$entry_link]
\n";
+ }
+ }
+ echo "
\n";
+ form();
+ echo "Add a race entry: ";
+ echo "\n";
+ echo "Ship: \n";
+ submit("add_entry", "Add");
+ echo "
\n";
+ end_form();
+ }
+
+?>
diff --git a/lib/footer.php b/lib/footer.php
new file mode 100644
index 0000000..3f7b97a
--- /dev/null
+++ b/lib/footer.php
@@ -0,0 +1,7 @@
+\n";
+ }
+?>
+