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 "\n"; + echo "\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 ""; + echo "\n"; /* Distance column. */ + echo "\n"; + echo "
StationLeg

"; + 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 "

total distance

\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 "\n"; + echo "\n"; + echo "\n"; + } + echo "
$stat

\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"; + } +?> + + diff --git a/lib/forms.php b/lib/forms.php new file mode 100644 index 0000000..c71c11f --- /dev/null +++ b/lib/forms.php @@ -0,0 +1,88 @@ +\n"; + } + + function end_form() { + echo "\n"; + } + + function input($name, $value = null, $type = null) { + 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 ""; + echo ""; + } + + function option($select, $value, $text, $selected = null) { + echo "