Working race entries and standings.
authorCMDR furrycat <elite@furrycat.net>
Fri, 8 Jan 2016 14:31:31 +0000 (09:31 -0500)
committerCMDR furrycat <elite@furrycat.net>
Fri, 8 Jan 2016 15:54:39 +0000 (10:54 -0500)
23 files changed:
entry.js [new file with mode: 0644]
index.php [new file with mode: 0644]
lib/.htaccess [new file with mode: 0644]
lib/competitor.php [new file with mode: 0644]
lib/elite_time.php [new file with mode: 0644]
lib/endorsements.php [new file with mode: 0644]
lib/entry.php [new file with mode: 0644]
lib/footer.php [new file with mode: 0644]
lib/forms.php [new file with mode: 0644]
lib/header.php [new file with mode: 0644]
lib/hull.php [new file with mode: 0644]
lib/include.php [new file with mode: 0644]
lib/lap.php [new file with mode: 0644]
lib/race.php [new file with mode: 0644]
lib/raikogram.php [new file with mode: 0644]
lib/ship.php [new file with mode: 0644]
propel/.htaccess [new file with mode: 0644]
propel/build/classes/BuckyballObject.php [new file with mode: 0644]
propel/build/classes/buckyball/Competitor.php [new file with mode: 0644]
propel/build/classes/buckyball/Entry.php [new file with mode: 0644]
propel/schema.xml
race.js [new file with mode: 0644]
style.css [new file with mode: 0644]

diff --git a/entry.js b/entry.js
new file mode 100644 (file)
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 (file)
index 0000000..24ef45c
--- /dev/null
+++ b/index.php
@@ -0,0 +1,29 @@
+<?php
+  date_default_timezone_set("UTC");
+  $propel_project = "buckyball";
+  $www_root = "members/furrycat/buckyball";
+  $root = join(DIRECTORY_SEPARATOR, array($_SERVER['DOCUMENT_ROOT'], $www_root));
+  $propel_root = join(DIRECTORY_SEPARATOR, array($root, "propel"));
+  $lib_root = join(DIRECTORY_SEPARATOR, array($root, "lib"));
+  require_once(join(DIRECTORY_SEPARATOR, array($root, "vendor", "autoload.php")));
+
+  Propel::init("$propel_root/build/conf/$propel_project-conf.php");
+  set_include_path(join(PATH_SEPARATOR, array(join(DIRECTORY_SEPARATOR, array($propel_root, "build", "classes")), get_include_path())));
+
+  include_once(join(DIRECTORY_SEPARATOR, array($lib_root, "include.php")));
+
+  parse_str($_SERVER['QUERY_STRING'], $params);
+  $module = $params['module'];
+  $action = $params['action'];
+
+  include_once(join(DIRECTORY_SEPARATOR, array($lib_root, "header.php")));
+  if ($module) {
+    $include = join(DIRECTORY_SEPARATOR, array($lib_root, "$module.php"));
+    if (file_exists($include)) {
+      include_once($include);
+      call_user_func("module_$module", $action);
+    }
+    else echo "<p>What's all that about, then?</p>\n";
+  }
+  include_once(join(DIRECTORY_SEPARATOR, array($lib_root, "footer.php")));
+?>
diff --git a/lib/.htaccess b/lib/.htaccess
new file mode 100644 (file)
index 0000000..93169e4
--- /dev/null
@@ -0,0 +1,2 @@
+Order deny,allow
+Deny from all
diff --git a/lib/competitor.php b/lib/competitor.php
new file mode 100644 (file)
index 0000000..53aa136
--- /dev/null
@@ -0,0 +1,48 @@
+<h2>Competitors</h2>
+<?php
+
+  function add_competitor($cmdr_name, $forum_name) {
+    if (! $cmdr_name) {
+      echo "<p>Missing CMDR name!</p>\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 "<p>Error adding competitor: " . $e->getMessage() . "</p>\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 "<p>No competitors</p>\n";
+    foreach ($competitors as $competitor) {
+      $cmdr = $competitor->getCmdrName();
+      echo "<p>CMDR <strong>$cmdr</strong>";
+      $forum = $competitor->getForumName();
+      if ($forum && $forum != $cmdr) echo " ($forum)";
+      "</p>\n";
+    }
+
+    echo "<hr>\n";
+    form();
+    echo "<p>Add a new competitor: CMDR ";
+    input("cmdr_name", $_POST['cmdr_name']);
+    echo " forum: ";
+    input("forum_name", $_POST['forum_name']);
+    submit("add_competitor", "Add");
+    echo "</p>\n";
+    end_form();
+  }
+?>
diff --git a/lib/elite_time.php b/lib/elite_time.php
new file mode 100644 (file)
index 0000000..9930334
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+
+  function elite_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 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 (file)
index 0000000..b09c973
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+
+  $ENDORSEMENTS = array(
+    'c' => 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 (file)
index 0000000..7d212b4
--- /dev/null
@@ -0,0 +1,304 @@
+<h2>Entries</h2>
+<?php
+
+  function retrieve_entry($entry) {
+    $_POST['add_entry'] = 'retrieve_entry';
+    $station_order = array();
+    $allotted_time = $entry->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 "<script type=\"text/javascript\"><!--\n";
+    echo "var distances = " . json_encode($distances) . ";\n";
+    echo "var allotted_time = $allotted_time;\n";
+    if ($entry) retrieve_entry($entry);
+    if ($_POST['lap1station1']) {
+      /*
+        XXX: Trust client validation only because furrycat is the only user
+        and he can vouch for himself.
+      */
+      if ($_POST['station_order']) $encoded_station_order = $_POST['station_order'];
+      else $encoded_station_order = json_encode($station_order);
+      $posted = array('station_order' => $encoded_station_order);
+      foreach ($_POST as $k => $v) {
+        if (! preg_match('/^(?:lap\d+(?:hull|station\d+$)|endorsement_|stat_)/', $k, $m)) continue;
+        if (! $v) continue;
+        $posted[$k] = $v;
+      }
+      echo "var posted = " . json_encode($posted) . ";\n";
+      echo "//--></script>\n";
+      if ($entry) {
+        $entry_id = $entry->getId();
+        $ship_name = $entry->getShip()->getName();
+        $cmdr_name = $entry->getShip()->getCompetitor()->getCmdrName();
+        echo "<p>Entry $entry_id: CMDR <strong>$cmdr_name</strong> flying <strong>$ship_name</strong></p>\n";
+      }
+      else if (do_entry($race_id, $ship_id, $posted, $allotted_time)) return true;
+    }
+    else echo "//--></script>\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 "<p>New entry: CMDR <strong>$cmdr_name</strong> flying <strong>$ship_name</strong></p>\n";
+    }
+    form();
+    echo "<div id=\"laps\">\n";
+    echo "<table>\n";
+    echo "<th>Station</th>\n";
+    echo "<th>Leg</th>\n";
+    $n = 0;
+    foreach ($legs as $station => $attributes) {
+      list($flags, $id, $system_id) = $attributes;
+      $n++;
+      $label = $station;
+      $class = ($flags & 2) ? ' class="start_line"' : '';
+      echo "<tr>\n";
+      echo "<td$class><p class=\"station$id system$system_id\">$station</p></td>\n";
+      echo "<td id=\"distance$n\"><p></p></td>\n";
+      echo "</tr>\n";
+    }
+    foreach ($legs as $station => $attributes) {
+      list($flags, $id, $system_id) = $attributes;
+      if ($flags & 1) continue;
+      $class = ' class="finish_line"';
+      echo "<tr>\n";
+      echo "<td$class><p class=\"station$id system$system_id\">$station</p></td>\n";
+      echo "<td id=\"distance$n\"><p></p></td>\n";
+      echo "</tr>\n";
+    }
+    echo "<tr>\n";
+    echo "<td>";
+    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 "</td>";
+    echo "<td><p>total distance</p></td>\n"; /* Distance column. */
+    echo "</tr>\n";
+    echo "</table>\n";
+    echo "<p>";
+    foreach (array('d', 'x') as $e) {
+      $name = "endorsement_$e";
+      $description = ucfirst(endorsement_description($e));
+      echo "$description: <input type=\"checkbox\" name=\"$name\"";
+      if ($_POST[$name]) echo " checked=\"checked\"";
+      echo ">\n";
+    }
+    echo "</p>\n";
+    end_form();
+    echo "</div>\n";
+    echo "<div id=\"stats\">\n";
+    echo "<table>\n";
+    foreach ($stats as $stat) {
+      $id = preg_replace('/[^a-z0-9]/', '_', strtolower($stat));
+      echo "<tr>\n";
+      echo "<th>$stat</th>\n";
+      echo "<td><p id=\"$id\"></p></td>\n";
+      echo "</tr>\n";
+    }
+    echo "</table>\n";
+    echo "</div>\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 "<p>Add races before submitting entries.</p>\n";
+      return;
+    }
+    $sq = new ShipQuery;
+    $sq->joinWith("Ship.Competitor");
+    $ships = $sq->orderBy('Competitor.CmdrName')->orderByName()->find();
+    if (! count($ships)) {
+      echo "<p>Add ships before submitting entries.</p>\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 "<p>CMDR <strong>$cmdr_name</strong> flying <strong>$ship_name</strong> in <strong>$race_name</strong> [$entry_link]</p>\n";
+      }
+    }
+    echo "<hr>\n";
+    form();
+    echo "<p>Add a race entry: ";
+    echo "<select name=\"race_id\">\n";
+    option("race_id", 0, "");
+    foreach ($races as $race) {
+      $id = $race->getId();
+      $name = $race->getName();
+      option("race_id", $id, $name);
+    }
+    echo "</select>\n";
+    echo "<a href=\"?module=ship\">Ship:</a> <select name=\"ship_id\">\n";
+    option("ship_id", 0, "");
+    $last_competitor = null;
+    foreach ($ships as $ship) {
+      $id = $ship->getId();
+      $ship_name = $ship->getName();
+      $competitor = $ship->getCompetitor();
+      $cmdr_name = $competitor->getCmdrName();
+      if ($cmdr_name != $last_competitor) {
+        if ($last_competitor) echo "</optgroup>\n";
+        echo "<optgroup label=\"CMDR $cmdr_name\">\n";
+      }
+      $last_competitor = $cmdr_name;
+      option("ship_id", $id, $ship_name);
+    }
+    if ($last_competitor) echo "</optgroup>\n";
+    echo "</select>\n";
+    submit("add_entry", "Add");
+    echo "</p>\n";
+    end_form();
+  }
+
+?>
diff --git a/lib/footer.php b/lib/footer.php
new file mode 100644 (file)
index 0000000..3f7b97a
--- /dev/null
@@ -0,0 +1,7 @@
+<?php
+  if ($module && file_exists("$root/$module.js")) {
+    echo "<script type=\"text/javascript\" src=\"$module.js\"></script>\n";
+  }
+?>
+</body>
+</html>
diff --git a/lib/forms.php b/lib/forms.php
new file mode 100644 (file)
index 0000000..c71c11f
--- /dev/null
@@ -0,0 +1,88 @@
+<?php
+
+  function form($classes = null, $action = null) {
+    if (is_null($classes)) $classes = array();
+    else if (! is_array($classes)) $classes = explode('/\s+/', $classes);
+    if (! isset($action)) $action = $_SERVER['REQUEST_URI'];
+    echo "<form ";
+    if (count($classes)) printf("class=\"%s\" ", implode(" ", $classes));
+    echo "method=\"POST\" action=\"$action\">\n";
+  }
+
+  function end_form() {
+    echo "</form>\n";
+  }
+
+  function input($name, $value = null, $type = null) {
+    echo "<input name=\"$name\"";
+    if (isset($type)) echo " type=\"$type\"";
+    if (isset($value)) echo " value=\"$value\"";
+    else echo " value=\"" . $_POST[$name] . "\"";
+    echo ">";
+  }
+
+  function hidden($name, $value = null) {
+    return input($name, $value, "hidden");
+  }
+
+  function submit($name, $value = null) {
+    return input($name, $value, "submit");
+  }
+
+  function textarea($name, $value = null) {
+    $id = "textarea_$name";
+    echo "<textarea id=\"$id\" name=\"$name\" cols=30>";
+    if (isset($value)) echo $value;
+    else echo $_POST[$name];
+    echo "</textarea>";
+    echo "<script>\n  $(function() {\n";
+    echo "    $(\"#$id\").autosize();\n";
+    echo "  });</script>";
+  }
+
+  function option($select, $value, $text, $selected = null) {
+    echo "  <option value=\"$value\"";
+    if (! isset($selected)) $selected = $_POST[$select];
+    if ($value == $selected) echo " selected";
+    echo ">$text\n";
+  }
+
+  function datepicker($name, $value = null, $past = true, $past_picker = null, $future = true, $future_picker = null) {
+    $id = "datepicker_$name";
+    echo "<script>\n  $(function() {\n";
+    echo "    $(\"#$id\").datepicker({\n";
+    /*
+      If this is the "to" picker in a date range, restrict the end date of the
+      "from" picker to be the date selected by this picker.
+    */
+    if (isset($past_picker)) {
+      echo "      onClose: function(selectedDate) {\n";
+      echo "        $(\"#datepicker_$past_picker\").datepicker(\"option\", \"maxDate\", selectedDate);\n";
+      echo "      },\n";
+    }
+    /*
+      If this is the "from" picker in a date range, restrict the start date of
+      the "to" picker to be the date selected by this picker.
+    */
+    if (isset($future_picker)) {
+      echo "      onClose: function(selectedDate) {\n";
+      echo "        $(\"#datepicker_$future_picker\").datepicker(\"option\", \"minDate\", selectedDate);\n";
+      echo "      },\n";
+    }
+    echo "      changeMonth: true,\n";
+    echo "      changeYear: true,\n";
+    echo "      dateFormat: 'yy-mm-dd',\n";
+    /* Are we allowed to show dates earlier than today? */
+    if (! $past) echo "      minDate: 0,\n";
+    /* Are we allowed to show dates later than today? */
+    if (! $future) echo "      maxDate: 0,\n";
+    echo "      showButtonPanel: true\n";
+    echo "    });\n  });</script>";
+    echo "<input name=\"$name\" id=\"$id\"";
+    echo " maxlength=10 size=10";
+    if (isset($value)) echo " value=\"$value\"";
+    else echo " value=\"" . $_POST[$name] . "\"";
+    echo "><em class=\"small\">(Y-m-d)</em>";
+  }
+
+?>
diff --git a/lib/header.php b/lib/header.php
new file mode 100644 (file)
index 0000000..8788241
--- /dev/null
@@ -0,0 +1,14 @@
+<head>
+<link rel="stylesheet" type="text/css" href="style.css">
+<title><?php echo "Buckyball Racing Club"; if ($module) echo " - $module"; ?></title>
+</head>
+<body>
+<?php
+  echo "<p>Buckyball Racing Club";
+  foreach (array("competitor", "hull", "ship", "system", "race", "raikogram", "entry") as $what) {
+    $title = preg_replace('/y$/', 'ie', $what);
+    echo " | <a href=\"?module=$what\">" . ucfirst($title) . "s</a>";
+  }
+  echo "</p>\n";
+?>
+<hr>
diff --git a/lib/hull.php b/lib/hull.php
new file mode 100644 (file)
index 0000000..d8919c3
--- /dev/null
@@ -0,0 +1,42 @@
+<h2>Hulls</h2>
+<?php
+
+  function add_hull($name) {
+    if (! $name) {
+      echo "<p>Missing hull name!</p>\n";
+      return false;
+    }
+    $hull = new Hull;
+    $hull->setName($name);
+    try {
+      $hull->save();
+      return true;
+    }
+    catch (Exception $e) {
+      echo "<p>Error adding hull: " . $e->getMessage() . "</p>\n";
+    }
+    return false;
+  }
+
+  function module_hull($action) {
+    if ($_POST['add_hull']) {
+      if (add_hull($_POST["name"])) unset($_POST);
+    }
+
+    $q = new HullQuery;
+    $hulls = $q->find();
+    if (! count($hulls)) echo "<p>No hulls</p>\n";
+    foreach ($hulls as $hull) {
+      $name = $hull->getName();
+      echo "<p><strong>$name</strong></p>\n";
+    }
+
+    echo "<hr>\n";
+    form();
+    echo "<p>Add a new hull: ";
+    input("name", $_POST['name']);
+    submit("add_hull", "Add");
+    echo "</p>\n";
+    end_form();
+  }
+?>
diff --git a/lib/include.php b/lib/include.php
new file mode 100644 (file)
index 0000000..f9a44ae
--- /dev/null
@@ -0,0 +1,5 @@
+<?php
+  include_once(join(DIRECTORY_SEPARATOR, array($lib_root, "endorsements.php")));
+  include_once(join(DIRECTORY_SEPARATOR, array($lib_root, "elite_time.php")));
+  include_once(join(DIRECTORY_SEPARATOR, array($lib_root, "forms.php")));
+?>
diff --git a/lib/lap.php b/lib/lap.php
new file mode 100644 (file)
index 0000000..c01a7dd
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+
+  function module_lap($action) {
+
+    $rq = new RaceQuery;
+    $races = $rq->find();
+    if (! count($races)) {
+      echo "<p>Add races before submitting laps.</p>\n";
+      return;
+    }
+    $sq = new ShipQuery;
+    $sq->joinWith("Ship.Competitor");
+    $ships = $sq->orderBy('Competitor.CmdrName')->orderByName()->find();
+    if (! count($ships)) {
+      echo "<p>Add ships before submitting laps.</p>\n";
+      return;
+    }
+    echo "<hr>\n";
+    form();
+    echo "<p>Add a race entry: ";
+    echo "<select name=\"race_id\">\n";
+    option("race_id", 0, "");
+    foreach ($races as $race) {
+      $id = $race->getId();
+      $name = $race->getName();
+      option("race_id", $id, $name);
+    }
+    echo "</select>\n";
+    echo "<a href=\"?module=ship\">Ship:</a> <select name=\"ship_id\">\n";
+    option("ship_id", 0, "");
+    $last_competitor = null;
+    foreach ($ships as $ship) {
+      $id = $ship->getId();
+      $ship_name = $ship->getName();
+      $competitor = $ship->getCompetitor();
+      $cmdr_name = $competitor->getCmdrName();
+      if ($cmdr_name != $last_competitor) {
+        if ($last_competitor) echo "</optgroup>\n";
+        echo "<optgroup label=\"CMDR $cmdr_name\">\n";
+      }
+      $last_competitor = $cmdr_name;
+      option("ship_id", $id, $ship_name);
+    }
+    if ($last_competitor) echo "</optgroup>\n";
+    echo "</select>\n";
+    submit("add_hull", "Add");
+    echo "</p>\n";
+    end_form();
+  }
+
+?>
diff --git a/lib/race.php b/lib/race.php
new file mode 100644 (file)
index 0000000..5ddd25a
--- /dev/null
@@ -0,0 +1,168 @@
+<h2>Races</h2>
+<?php
+
+  function entry_bgcolor($row, $column) {
+    $blue = array('#006ecb', '#005195');
+    $white = array('#ffffff', '#f5f5f5');
+    $orange = array('#ff6418', '#c74e13');
+    $colours = array(
+      array_merge($blue, $blue, $blue, $blue, $white, $white, $white, $orange, $orange),
+      array('#d9d9d9', '#d4d4d4'),
+      array('#ebebeb', '#e6e6e6')
+    );
+    if (! $row) $row_index = 0;
+    else $row_index = 1 + ($row % 2);
+    $column_index = $column % count($colours[$row_index]);
+    return $colours[$row_index][$column_index];
+  }
+
+  function entry_fgcolor($row, $column, $header = 0) {
+    $white = array('#ffffff');
+    $blue = array('#005195');
+    $orange = array('#ff6418');
+    $colours = array(
+      array_merge($white, $white, $white, $white, $white, $white, $white, $white, $blue, $blue, $blue,$blue, $blue,  $blue, $white, $white, $white),
+      $blue,
+      $orange
+    );
+    if (! $row) $row_index = 0;
+    else $row_index = 1 + ($row % 2);
+    $column_index = $column % count($colours[$row_index]);
+    return $colours[$row_index][$column_index];
+  }
+
+  function entry_header($type) {
+    $fields = array('P', 'E', 'CMDR name', 'Forum name', 'Ship type', 'Ship name', 'Distance', 'Penalty', 'Lap distance', 'Laps', 'Allotted', 'Bonus', 'Raced', 'Total', 'Fastest lap', 'Average lap', 'Best speed', 'Average speed');
+    $small_fields = array('Distance', 'Penalty', 'Allotted', 'Lap distance', 'Fastest lap', 'Average lap', 'Average speed');
+    if ($type == 'html') {
+      echo "<table class=\"entries\">\n";
+      echo "<tr>\n";
+      $column = 0;
+      foreach ($fields as $field) {
+        $bgcolor = entry_bgcolor(0, $column);
+        $fgcolor = entry_fgcolor(0, $column);
+        echo "<th style=\"background-color: $bgcolor; color: $fgcolor\">$field</th>\n";
+        $column++;
+      }
+      echo "</tr>\n";
+    }
+    else if ($type == 'bbcode') {
+      echo "<pre>\n";
+      echo "[table=\"class: outer_border\"]\n";
+      echo "[tr]\n";
+      $column = 0;
+      foreach ($fields as $field) {
+        $bgcolor = entry_bgcolor(0, $column);
+        $fgcolor = entry_fgcolor(0, $column);
+        echo "[td=\"bgcolor: $bgcolor\"][color=$fgcolor]";
+        if (in_array($field, $small_fields)) {
+          echo "[size=-2]";
+          echo $field;
+          echo "[/size]";
+        }
+        else echo $field;
+        echo "[/color][/td]\n";
+        $column++;
+      }
+      echo "[/tr]\n";
+    }
+  }
+
+  function entry_footer($type) {
+    if ($type == 'html') {
+      echo "</table>\n";
+    }
+    else if ($type == 'bbcode') {
+      echo "[/table]\n";
+      echo "</pre>\n";
+    }
+  }
+
+  function show_entry($entry, $allotted_time, $position, $row, $type) {
+    $fields = array();
+    $endorsements = $entry->getEndorsements();
+    $fields[] = $position;
+    $fields[] = endorsement_string($endorsements);
+    $ship = $entry->getShip();
+    $competitor = $ship->getCompetitor();
+    $cmdr_name= $competitor->getCmdrName();
+    $fields[] = $cmdr_name;
+    $forum_name = $competitor->getForumName();
+    if (! $forum_name) $forum_name = $cmdr_name;
+    $fields[] = $forum_name;
+    $hull = $ship->getHull();
+    $fields[] = $hull->getName();
+    $fields[] = $ship->getName();
+    $fields[] = format_distance($entry->getTotalDistance());
+    $fields[] = format_time($entry->getPenaltyTime());
+    $fields[] = format_distance($entry->getLapDistance());
+    $fields[] = $entry->getCompleteLaps();
+    $fields[] = ($allotted_time > 0) ? format_time($allotted_time) : '';
+    $fields[] = ($allotted_time > 0) ? format_time($entry->getBonusTime()) : '';
+    $fields[] = format_time($entry->getRacedTime());
+    $fields[] = ($allotted_time > 0) ? format_time($entry->getTotalTime()) : '';
+    $fields[] = format_time($entry->getFastestLap());
+    $fields[] = format_time($entry->getAverageLap());
+    $fields[] = format_speed($entry->getBestSpeed());
+    $fields[] = format_speed($entry->getAverageSpeed());
+
+    if ($type == 'html') {
+      echo "<tr>\n";
+      $column = 0;
+      foreach ($fields as $field) {
+        $bgcolor = entry_bgcolor($row, $column);
+        $fgcolor = entry_fgcolor($row, $column);
+        echo "<td style=\"background-color: $bgcolor; color: $fgcolor\">$field</td>\n";
+        $column++;
+      }
+      echo "</tr>\n";
+    }
+    else if ($type == 'bbcode') {
+      echo "[tr]\n";
+      $column = 0;
+      foreach ($fields as $field) {
+        $bgcolor = entry_bgcolor($row, $column);
+        $fgcolor = entry_fgcolor($row, $column);
+        echo "[td=\"bgcolor: $bgcolor\"][color=$fgcolor]";
+        echo $field;
+        echo "[/color][/td]\n";
+        $column++;
+      }
+      echo "[/tr]\n";
+    }
+  }
+
+  function module_race($action) {
+    $rq = new RaceQuery;
+    if ($action) $rq->filterById($action);
+    $races = $rq->find();
+    foreach ($races as $race) {
+      $id = $race->getId();
+      $name = $race->getName();
+      $allotted_time = $race->getAllottedTime();
+      $eq = new EntryQuery;
+      $eq->joinWith('Entry.Ship');
+      $eq->joinWith('Ship.Competitor');
+      $eq->joinWith('Ship.Hull');
+      $eq->filterByRaceId($id);
+      $entries = $eq->orderByTotalDistance('desc')->orderByPenaltyTime('desc')->orderByTotalTime('desc')->find();
+      if (! count($entries) && ! $action) continue;
+
+      echo "<h3>$name</h3>\n";
+      echo "<div class=\"classification\">\n";
+      foreach (array('html', 'bbcode') as $type) {
+        echo "<div class=\"$type\">\n";
+        entry_header($type);
+        $n = $position = 1;
+        foreach ($entries as $entry) {
+          $endorsements = $entry->getEndorsements();
+          show_entry($entry, $allotted_time, endorsement('x', $endorsements) ? '-' : $position++, $n++, $type);
+        }
+        entry_footer($type);
+        echo "</div>\n";
+      }
+      echo "</div>\n";
+    }
+  }
+
+?>
diff --git a/lib/raikogram.php b/lib/raikogram.php
new file mode 100644 (file)
index 0000000..5050c29
--- /dev/null
@@ -0,0 +1,97 @@
+<h2>Raikograms</h2>
+<?php
+
+  function edit_raikogram($race_id) {
+    $q = new CourseQuery;
+    $q->joinWith("Course.Station");
+    $q->join("Station.System");
+    $q->withColumn("System.Id", "SystemId");
+    $q->withColumn("System.Name", "SystemName");
+    $q->findByRaceId($race_id);
+    $course = $q->find();
+    $systems = array();
+    foreach ($course as $leg) {
+      $station = $leg->getStation();
+      $id = $leg->getSystemId();
+      $name = $leg->getSystemName();
+      $systems[$name] = $id;
+    }
+    ksort($systems);
+    if ($_POST['edit_raikogram'] == 'Update') {
+      foreach ($_POST as $k => $v) {
+        if (! preg_match('/^from(\d+)to(\d+)$/', $k, $m)) continue;
+        if (! $v) continue;
+        list($ignored, $a, $b) = $m;
+        $system1 = min($a, $b);
+        $system2 = max($a, $b);
+        $rq = new RaikogramQuery;
+        $rq->filterBySystem1($system1);
+        $rq->filterBySystem2($system2);
+        $raikogram = $rq->findOne();
+        if (! $raikogram) $raikogram = new Raikogram;
+        $raikogram->setSystem1($system1);
+        $raikogram->setSystem2($system2);
+        $raikogram->setDistance($v);
+        $raikogram->save();
+      }
+    }
+    form();
+    echo "<table>\n";
+    /* Header. */
+    echo "<tr>\n";
+    echo "<th></th>\n";
+    foreach ($systems as $name_x => $id_x) echo "<th>$name_x</th>\n";
+    echo "</tr>\n";
+    foreach ($systems as $name_y => $id_y) {
+      echo "<tr>\n";
+      echo "<th>$name_y</th>\n";
+      foreach ($systems as $name_x => $id_x) {
+        echo "<td>\n";
+        if ($id_x == $id_y) continue;
+        $distance = "";
+        $rq = new RaikogramQuery;
+        $rq->filterBySystem1(min($id_x, $id_y));
+        $rq->filterBySystem2(max($id_x, $id_y));
+        $raikogram = $rq->findOne();
+        if ($raikogram) $distance = $raikogram->getDistance();
+        input("from${id_x}to${id_y}", $distance);
+        echo "</td>\n";
+      }
+      echo "</tr>\n";
+    }
+    echo "<td>";
+    echo "</table>\n";
+    hidden('race_id', $race_id);
+    submit("edit_raikogram", "Update");
+    end_form();
+    echo "<hr>\n";
+    return false;
+  }
+
+  function module_raikogram($action) {
+    if ($_POST['edit_raikogram']) {
+      if (edit_raikogram($_POST["race_id"])) unset($_POST);
+    }
+
+    $rq = new RaceQuery;
+    $races = $rq->find();
+    if (! count($races)) {
+      echo "<p>Add races before editing Raikograms.</p>\n";
+      return;
+    }
+    form();
+    echo "<p>Edit race raikogram: ";
+    echo "<select name=\"race_id\">\n";
+    option("race_id", 0, "");
+    foreach ($races as $race) {
+      $id = $race->getId();
+      $name = $race->getName();
+      option("race_id", $id, $name);
+    }
+    echo "</select>\n";
+    submit("edit_raikogram", "Edit");
+    echo "</p>\n";
+    end_form();
+  }
+
+?>
diff --git a/lib/ship.php b/lib/ship.php
new file mode 100644 (file)
index 0000000..e7ad6db
--- /dev/null
@@ -0,0 +1,80 @@
+<h2>Ships</h2>
+<?php
+
+  function add_ship($competitor_id, $hull_id, $name) {
+    if (! $name) {
+      echo "<p>Missing ship name!</p>\n";
+      return false;
+    }
+    $ship = new Ship;
+    $ship->setHullId($hull_id);
+    $ship->setCompetitorId($competitor_id);
+    $ship->setName($name);
+    try {
+      $ship->save();
+      return true;
+    }
+    catch (Exception $e) {
+      echo "<p>Error adding ship: " . $e->getMessage() . "</p>\n";
+    }
+    return false;
+  }
+
+  function module_ship($action) {
+    if ($_POST['add_ship']) {
+      if (add_ship($_POST["competitor_id"], $_POST["hull_id"], $_POST["name"])) unset($_POST);
+    }
+
+    $q = new ShipQuery;
+    $q->joinWith("Ship.Competitor");
+    $q->joinWith("Ship.Hull");
+    $ships = $q->orderByName()->find();
+    if (! count($ships)) echo "<p>No ships</p>\n";
+    foreach ($ships as $ship) {
+      $name = $ship->getName();
+      $hull = $ship->getHull();
+      $hull_name = $hull->getName();
+      $competitor = $ship->getCompetitor();
+      $cmdr = $competitor->getCmdrName();
+      echo "<p><strong>$name</strong> ($hull_name) CMDR $cmdr</p>\n";
+    }
+
+    echo "<hr>\n";
+    $hq = new HullQuery;
+    $hulls = $hq->find();
+    $cq = new CompetitorQuery;
+    $competitors = $cq->find();
+    if (! count($hulls)) {
+      echo "<p>Add hulls before adding ships.</p>\n";
+      return;
+    }
+    if (! count($competitors)) {
+      echo "<p>Add competitors before adding ships.</p>\n";
+      return;
+    }
+    form();
+    echo "<p>Add a new ship: ";
+    echo "<a href=\"?module=hull\">Hull:</a> <select name=\"hull_id\">";
+    option("hull_id", 0, "");
+    foreach ($hulls as $hull) {
+      $id = $hull->getId();
+      $name = $hull->getName();
+      option("hull_id", $id, $name);
+    }
+    echo "</select>\n";
+    echo "<a href=\"?module=competitor\">Competitor:</a> <select name=\"competitor_id\">";
+    option("competitor_id", 0, "");
+    foreach ($competitors as $competitor) {
+      $id = $competitor->getId();
+      $cmdr = $competitor->getCmdrName();
+      option("competitor_id", $id, $cmdr);
+    }
+    input("name", $_POST["name"]);
+    echo "</select>\n";
+
+    submit("add_ship", "Add");
+    echo "</p>\n";
+    end_form();
+  }
+?>
+
diff --git a/propel/.htaccess b/propel/.htaccess
new file mode 100644 (file)
index 0000000..93169e4
--- /dev/null
@@ -0,0 +1,2 @@
+Order deny,allow
+Deny from all
diff --git a/propel/build/classes/BuckyballObject.php b/propel/build/classes/BuckyballObject.php
new file mode 100644 (file)
index 0000000..18ba2e5
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+
+  class BuckyballObject extends BaseObject {
+    function getURL() {
+      /* XXX */
+      $class = get_class($this->getPeer());
+      return sprintf("?module=%s&action=%s", urlencode(strtolower($class::OM_CLASS)), $this->getId());
+      return sprintf("/%s/%s/%d", urlencode(strtolower($class::OM_CLASS)), urlencode($this->getName()), $this->getId());
+    }
+
+    function getLink($blurb = null, $url = null, $classes = null) {
+      if (is_null($classes)) $classes = array();
+      else if (! is_array($classes)) $classes = array($classes);
+      return sprintf("<a %shref=\"%s\">%s</a>", (count($classes)) ? "class=\"" . implode(" ", $classes) . "\" " : "", (isset($url)) ? $url : $this->getURL(), (isset($blurb)) ? htmlspecialchars($blurb) : htmlspecialchars($this->getName()));
+    }
+
+    function getStrongLink($blurb = null, $url = null) {
+      return $this->getLink($blurb, $url, "strong");
+    }
+
+    function getActionLink($action, $blurb, $classes = null) {
+      if (is_null($classes)) $classes = array();
+      else if (! is_array($classes)) $classes = array($classes);
+      return $this->getLink($blurb, sprintf("%s/%s", $this->getURL(), urlencode($action)), array_unique(array_merge($classes, array("small"))));
+    }
+
+    function getDeleteLink($confirm = false) {
+      $link = ($confirm) ? "confirmdelete" : "delete";
+      return $this->getActionLink($link, "Delete", array("delete", "noprint"));
+    }
+  }
+
+?>
diff --git a/propel/build/classes/buckyball/Competitor.php b/propel/build/classes/buckyball/Competitor.php
new file mode 100644 (file)
index 0000000..370537c
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+
+
+
+/**
+ * Skeleton subclass for representing a row from the 'competitor' table.
+ *
+ *
+ *
+ * You should add additional methods to this class to meet the
+ * application requirements.  This class will only be generated as
+ * long as it does not already exist in the output directory.
+ *
+ * @package    propel.generator.buckyball
+ */
+class Competitor extends BaseCompetitor
+{
+  function getName() {
+    return $this->getCmdrName();
+  }
+}
diff --git a/propel/build/classes/buckyball/Entry.php b/propel/build/classes/buckyball/Entry.php
new file mode 100644 (file)
index 0000000..056458f
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+
+
+
+/**
+ * Skeleton subclass for representing a row from the 'entry' table.
+ *
+ *
+ *
+ * You should add additional methods to this class to meet the
+ * application requirements.  This class will only be generated as
+ * long as it does not already exist in the output directory.
+ *
+ * @package    propel.generator.buckyball
+ */
+class Entry extends BaseEntry
+{
+  function getName() {
+    return sprintf("entry %d", $this->getId());
+  }
+}
index 5a4bdc3..23b8ffc 100644 (file)
@@ -90,6 +90,7 @@
     </vendor>
     <column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true"/>
     <column name="name" type="varchar" size="64" required="true"/>
+    <column name="allotted_time" type="integer" required="false"/>
     <unique name="name">
       <unique-column name="name"/>
     </unique>
       <unique-column name="station_id"/>
     </unique>
     <unique name="start_line">
+      <unique-column name="race_id"/>
       <unique-column name="start_line"/>
     </unique>
     <unique name="finish_line">
+      <unique-column name="race_id"/>
       <unique-column name="finish_line"/>
     </unique>
     <foreign-key foreignTable="race" phpName="Race" refPhpName="Course">
       <parameter name="Charset" value="utf8"/>
     </vendor>
     <column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true"/>
-    <column name="race_id" type="integer" required="true"/>
-    <column name="ship_id" type="integer" required="true"/>
+    <column name="entry_id" type="integer" required="true"/>
     <column name="lap_number" type="integer" required="true"/>
-    <unique name="ship_lap">
-      <unique-column name="race_id"/>
-      <unique-column name="ship_id"/>
+    <column name="hull_start" type="integer"/>
+    <column name="hull_end" type="integer"/>
+    <unique name="entry_lap">
+      <unique-column name="entry_id"/>
       <unique-column name="lap_number"/>
     </unique>
-    <foreign-key foreignTable="race" phpName="Race" refPhpName="Lap">
-      <reference local="race_id" foreign="id"/>
-    </foreign-key>
-    <foreign-key foreignTable="ship" phpName="Ship" refPhpName="Lap">
-      <reference local="ship_id" foreign="id"/>
+    <foreign-key foreignTable="entry" phpName="Entry" refPhpName="Lap">
+      <reference local="entry_id" foreign="id"/>
     </foreign-key>
   </table>
 
     </vendor>
     <column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true"/>
     <column name="lap_id" type="integer" required="true"/>
+    <column name="station_order" type="integer" required="true"/>
     <column name="station_id" type="integer" required="true"/>
     <column name="arrival" type="timestamp" required="true"/>
     <unique name="lap_arrival">
     </unique>
     <unique name="lap_station">
       <unique-column name="lap_id"/>
-      <unique-column name="station_id"/>
+      <unique-column name="station_order"/>
     </unique>
     <foreign-key foreignTable="lap" phpName="Lap" refPhpName="Laptime">
       <reference local="lap_id" foreign="id"/>
     </foreign-key>
   </table>
 
+  <!-- Race entry -->
+  <table name="entry" phpName="Entry" baseClass="BuckyballObject">
+    <vendor type="mysql">
+      <parameter name="Engine" value="InnoDB"/>
+      <parameter name="Charset" value="utf8"/>
+    </vendor>
+    <column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true"/>
+    <column name="race_id" type="integer" required="true"/>
+    <column name="ship_id" type="integer" required="true"/>
+    <column name="endorsements" type="integer"/>
+    <column name="lap_distance" type="float" required="true"/>
+    <column name="complete_laps" type="integer" required="true"/>
+    <column name="total_distance" type="float" required="true"/>
+    <column name="penalty_time" type="integer"/>
+    <column name="bonus_time" type="integer"/>
+    <column name="raced_time" type="integer" required="true"/>
+    <column name="total_time" type="integer" required="true"/>
+    <column name="fastest_lap" type="integer" required="true"/>
+    <column name="average_lap" type="integer" required="true"/>
+    <column name="best_speed" type="float" required="true"/>
+    <column name="average_speed" type="float" required="true"/>
+    <foreign-key foreignTable="race" phpName="Race" refPhpName="Entry">
+      <reference local="race_id" foreign="id"/>
+    </foreign-key>
+    <foreign-key foreignTable="ship" phpName="Ship" refPhpName="Entry">
+      <reference local="ship_id" foreign="id"/>
+    </foreign-key>
+  </table>
+
+  <!-- Distance between stations -->
+  <table name="raikogram" phpName="Raikogram" baseClass="BuckyballObject">
+    <vendor type="mysql">
+      <parameter name="Engine" value="InnoDB"/>
+      <parameter name="Charset" value="utf8"/>
+    </vendor>
+    <column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true"/>
+    <column name="system1" type="integer" required="true"/>
+    <column name="system2" type="integer" required="true"/>
+    <column name="distance" type="float" required="true"/>
+    <unique name="systems">
+      <unique-column name="system1"/>
+      <unique-column name="system2"/>
+    </unique>
+    <foreign-key foreignTable="system" phpName="LowSystem" refPhpName="LowRaikogram">
+      <reference local="system1" foreign="id"/>
+    </foreign-key>
+    <foreign-key foreignTable="system" phpName="HighSystem" refPhpName="HighRaikogram">
+      <reference local="system2" foreign="id"/>
+    </foreign-key>
+  </table>
+
 </database>
diff --git a/race.js b/race.js
new file mode 100644 (file)
index 0000000..de31119
--- /dev/null
+++ b/race.js
@@ -0,0 +1,46 @@
+/* Copy table to clipboard. */
+function copy_classification(event) {
+  try {
+    var text = event.currentTarget.getElementsByClassName('bbcode')[0].getElementsByTagName('pre')[0].innerHTML;
+    if (text) {
+      var area = document.createElement('textarea');
+      area.style.position = 'fixed';
+      area.style.top = 0;
+      area.style.left = 0;
+      area.style.height = '2em';
+      area.style.width = '2em';
+      area.style.padding = 0;
+      area.style.background = 'transparent';
+      area.value = text;
+      document.body.appendChild(area);
+      area.select();
+      if (document.execCommand('copy')) console.log('Copied bbcode to clipboard.');
+      else console.log('Failed to copy bbcode to clipboard.');
+      document.body.removeChild(area);
+    }
+  }
+  catch (e) {
+    console.log(e);
+  }
+}
+
+/* Switch between HTML and bbcode. */
+function toggle_classification(event) {
+  for (div of event.currentTarget.getElementsByTagName('div')) {
+    if (window.getComputedStyle(div).getPropertyValue('display') == 'none') {
+      div.style.display = 'block';
+    }
+    else {
+      div.style.display = 'none';
+    }
+  }
+}
+
+/* Document onload. */
+function loaded() {
+  for (classification of document.getElementsByClassName('classification')) {
+    classification.addEventListener('dblclick', copy_classification, true);
+  }
+}
+
+document.onload = loaded();
diff --git a/style.css b/style.css
new file mode 100644 (file)
index 0000000..28e9a62
--- /dev/null
+++ b/style.css
@@ -0,0 +1,9 @@
+#laps { overflow-x: auto; }
+#laps table { background-color: rgba(20, 50, 100, 0.2); }
+#laps th, #laps td { padding-left: 1em; }
+th { text-align: left; }
+th:nth-child(n+2),td:nth-child(n+2) { text-align: center; }
+#stats td:nth-child(n+1) { text-align: left; }
+table.entries { background-color: black; }
+table.entries th, table.entries td { text-align: center; }
+.classification .bbcode { display: none; background-color: rgba(0, 0, 0, 0.1); }