Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] グラフコースへの対応 #227

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion app/controllers/contests/problems_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,17 @@ def load_contest
# GET /contests/1/problems
# GET /contests/1/problems.json
def index
@problems = @contest.problems
@problems = @current_user.group.visible_problems_in(@contest)
@users = User.contestants_of(@contest)
@current_user.attend(@contest) unless @current_user.attended? @contest

group = @current_user.group
visible_problems = group.visible_problems_in(@contest)
solved_problems = group.solved_problems_in(@contest)
@json_nodes = build_json_nodes(@contest.problems, visible_problems, solved_problems)

@json_edges = JSON.generate(ProblemEdge.all.map { |pe| { f: pe.from_problem_id, t: pe.to_problem_id, curve: 0 } });

respond_to do |format|
format.html # index.html.erb
format.json { render json: @problems }
Expand Down Expand Up @@ -77,4 +84,27 @@ def check_attendance
redirect_to contests_path unless @current_user.attended? @contest
end

def build_json_nodes(problems, visible_problems, solved_problems)
def build_node(problem)
{
id: problem.id,
title: problem.title,
text: problem.description,
x: problem.x,
y: problem.y,
}
end

node_hash = Hash[*problems.flat_map { |p| [p.id, build_node(p)] }]
visible_problems.each do |p|
node_hash[p.id][:visible] = true
end
solved_problems.each do |p|
node_hash[p.id][:visible] = true
node_hash[p.id][:solved] = true
end

JSON.generate(node_hash.values)
end

end
12 changes: 12 additions & 0 deletions app/models/group.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ def attendances_for(contest)
Attendance.where(contest_id: contest.id, user_id: user_ids)
end

def solved_problems_in(contest)
att_ids = Attendance.where(user_id: user_ids).select(:id)
sbm_ids = Submission.where(solved: true, attendance_id: att_ids).select(:problem_id)
Problem.where(id: sbm_ids)
end

def visible_problems_in(contest)
solved_problems = solved_problems_in(contest).select(:id)
edges = ProblemEdge.where(from_problem_id: solved_problems)
Problem.where(id: edges)
end

def solved_submission_for(problem, type)
Submission.where(
problem_id: problem.id,
Expand Down
5 changes: 5 additions & 0 deletions app/models/problem.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ class Problem < ActiveRecord::Base
belongs_to :contest
has_many :submissions

has_many :edges_from, class_name: ProblemEdge, foreign_key: :to_problem_id, dependent: :destroy
has_many :edges_to, class_name: ProblemEdge, foreign_key: :from_problem_id, dependent: :destroy
has_many :from_problems, through: :edges_from, source: :from_problem
has_many :to_problems, through: :edges_to, source: :to_problem

before_save :convert_html

def prefix
Expand Down
6 changes: 6 additions & 0 deletions app/models/problem_edge.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class ProblemEdge < ActiveRecord::Base
attr_accessible :from_problem_id, :to_problem_id

belongs_to :from_problem, class_name: Problem, foreign_key: :from_problem_id
belongs_to :to_problem, class_name: Problem, foreign_key: :to_problem_id
end
178 changes: 177 additions & 1 deletion app/views/contests/problems/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
b ? content_tag(:div, 'Correct!', class: 'notification alert alert-success') : content_tag(:div, 'Wrong...', class: 'notification alert alert-error')
} %>

<h2>Problem Course</h2>
<div id="course-wrapper">
</div>

<h2 style="float: right;"><%= link_to 'Scoreboard', contest_score_path %></h2>

<h2>Problem List</h2>
Expand All @@ -17,7 +21,7 @@
<th class='center'>Solved Time</th>
</tr>

<% @problems.order(:title).each do |problem| %>
<% @problems.each do |problem| %>
<tr>
<td><%= link_to problem.title, contest_problem_path(problem.contest, problem) %></td>
<td class='center'>Small<br>Large</td>
Expand Down Expand Up @@ -71,3 +75,175 @@
<% end %>
</ul>

<script src="http://lodash.com/_js/lodash.compat.js" charset="utf-8"></script>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script>
(function () {
var Point = function (x, y) {
this.x = x;
this.y = y;
};

// useful alias
var pt = function (x, y) { return new Point(x, y); };

var hypot = function (a, b) { return Math.sqrt(a * a + b * b); };

Point.prototype.add = function(p) { return pt(this.x + p.x, this.y + p.y); };
Point.prototype.sub = function(p) { return pt(this.x - p.x, this.y - p.y); };
Point.prototype.mul = function(s) { return pt(this.x * s, this.y * s); };
Point.prototype.div = function(s) { return pt(this.x / s, this.y / s); };
Point.prototype.dot = function(p) { return this.x * p.x + this.y * p.y; };
Point.prototype.cross = function(p) { return this.x * p.y - this.y * p.x; };
Point.prototype.abs = function() { return hypot(this.x, this.y); };
Point.prototype.unit = function() { return this.div(this.abs()); };
Point.prototype.rotate = function(r) {
var s = Math.sin(r);
var c = Math.cos(r);
return pt(this.x * c - this.y * s, this.x * s + this.y * c);
};

(function () {
var nodes = <%= @json_nodes.html_safe %>;
var edges = <%= @json_edges.html_safe %>;

var nodesFromID = {};

_(nodes).each(function (n) {
n.pos = pt(n.x, n.y);
nodesFromID[n.id] = n;
});

_(edges).each(function (e) {
var t = nodesFromID[e.t].pos;
var f = nodesFromID[e.f].pos;
var d = t.sub(f).abs();
var h = t.sub(f).unit().rotate(Math.PI / 2);
e.control = t.add(f).div(2).add(h.mul(d*e.curve));
});

var hypot = function (a, b) {
return Math.sqrt(a * a + b * b);
};

var port = function (p1, p2, dist) {
var x1 = p1.x;
var y1 = p1.y;
var x2 = p2.x;
var y2 = p2.y;
var dx = x1 - x2;
var dy = y1 - y2;
var d = hypot(dx, dy);
return { x: x1 - dx * dist / d, y: y1 - dy * dist / d };
};

var showPopup = function (d) {
console.log(d);
var popup = d3.select('div#svg-wrapper')
.append('div')
.attr('class', 'course-popup')
.style('width', '300px')
.style('height', '200px')
.style('position', 'absolute')
.style('display', 'block')
.style('left', d.pos.x + 40 + 'px')
.style('top', d.pos.y + 'px')
.style('background', '#DEF')
.style('padding', '0 20px');

popup.append('h1')
.text(d.title);

popup.append('p')
.text(d.text);
};

var hidePopup = function (d) {
d3.selectAll('.course-popup').remove();
};

var goToProblem = function (d) {
alert('go to problem ' + d.id);
};

var edgeColor = '#456';
var visibleNodeColor = '#1abc9c';
var hiddenNodeColor = '#bdc3c7';

var nodeOuterColor = function (d) {
return d.visible ? visibleNodeColor :
hiddenNodeColor;
}

var nodeInnerColor = function (d) {
return d.solved ? 'white' :
d.visible ? visibleNodeColor :
hiddenNodeColor;
}

var svg = d3.select('#course-wrapper')
.append('div')
.attr('id', 'svg-wrapper')
.style('position', 'relative')
.append('svg')
.attr('id', 'problem-course')
.attr('width', 1200)
.attr('height', 600);

svg.append('rect')
.attr('width', '1200px')
.attr('height', '600px')
.attr('fill', '#f0f2f5');

svg.append('defs').append('marker')
.attr('id', 'arrow-head')
.attr('viewBox', '0 0 10 10')
.attr('refX', 6)
.attr('refY', 5)
.attr('markerWidth', '5')
.attr('markerHeight', '5')
.attr('orient', 'auto')
.append('polygon')
.attr('points', '0,0 10,5 0,10')
.attr('fill', edgeColor);

var render = function () {

svg.selectAll('circle')
.data(nodes)
.enter()
.append('circle')
.attr('stroke', nodeOuterColor)
.attr('stroke-width', 4)
.attr('fill', nodeInnerColor)
.attr('r', 20)
.attr('cx', function (d) { return d.pos.x; })
.attr('cy', function (d) { return d.pos.y; })
.on('mouseenter', showPopup)
.on('mouseleave', hidePopup)
.on('click', goToProblem)
.on('touch', goToProblem);

//M 25 25 Q 175 25 175 175
svg.selectAll('line')
.data(edges)
.enter()
.append('path')
.each(function (d) { d.dom = this; })
.style('stroke', edgeColor)
.style('fill', 'none')
.style('stroke-width', 3)
.attr('d', function (d) {
var p1 = port(nodesFromID[d.f].pos, d.control, 25);
var c = d.control;
var p2 = port(nodesFromID[d.t].pos, d.control, 30);
return 'M '+p1.x+' '+p1.y+' Q '+c.x+' '+c.y+' '+p2.x+' '+p2.y;
})
.attr('marker-end', 'url(#arrow-head)');
};

render();

})();
})();
</script>
10 changes: 10 additions & 0 deletions db/migrate/20140412105531_create_problem_edges.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class CreateProblemEdges < ActiveRecord::Migration
def change
create_table :problem_edges do |t|
t.integer :from_problem_id, null: false
t.integer :to_problem_id, null: false

t.timestamps
end
end
end
8 changes: 8 additions & 0 deletions db/migrate/20140412131533_add_column_x_and_y_to_problem.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class AddColumnXAndYToProblem < ActiveRecord::Migration
def up
change_table :problems do |t|
t.integer :x
t.integer :y
end
end
end
11 changes: 10 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
#
# It's strongly recommended to check this file into your version control system.

ActiveRecord::Schema.define(:version => 20131031122342) do
ActiveRecord::Schema.define(:version => 20140412131533) do

create_table "attendances", :force => true do |t|
t.integer "user_id", :null => false
Expand Down Expand Up @@ -40,6 +40,13 @@
t.text "output"
end

create_table "problem_edges", :force => true do |t|
t.integer "from_problem_id", :null => false
t.integer "to_problem_id", :null => false
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end

create_table "problems", :force => true do |t|
t.string "title"
t.text "description"
Expand All @@ -54,6 +61,8 @@
t.integer "contest_id"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
t.integer "x"
t.integer "y"
end

create_table "submissions", :force => true do |t|
Expand Down
33 changes: 32 additions & 1 deletion spec/factories/factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,38 @@
introduction 'Waseda University Programming Contest'
start_time Time.now
end_time Time.new(2114, 6, 2, 16, 0, 0)
problems { |c| c.problems = FactoryGirl.create_list(:problem, 15) }
problems { |c| c.problems = FactoryGirl.create_list(:problem, 22) }
after(:create) do |c, evaluator|
c.problems.each { |p| p.save } if evaluator.save_problems
# FIXME: (>_<)
edges = [
[1,3,6],
[2],
[9],
[4],
[7,5],
[9],
[7],
[8],
[9],
[10,12,15],
[11],
[21],
[13,18],
[14],
[21],
[16],
[17],
[18],
[19],
[20],
[21],
[]
]

edges.each_with_index do |es,i|
es.each { |e| ProblemEdge.create(from_problem_id: i+1, to_problem_id: e+1).save! }
end
end
end

Expand All @@ -53,6 +82,8 @@
sequence(:large_output) {|n| (100+n).to_s }
small_score 3
large_score 1020
sequence(:x) {|n| [nil, 45, 210, 430, 153, 261, 369, 261, 369, 477, 585, 800, 1017, 750, 909, 1080, 680, 780, 880, 950, 1050, 1150, 1233][n] }
sequence(:y) {|n| [nil, 295, 127, 127, 295, 252, 252, 414, 414, 393, 252, 60, 60, 252, 184, 184, 420, 440, 420, 330, 330, 300, 184][n] }
end

factory :image
Expand Down
8 changes: 8 additions & 0 deletions spec/factories/problem_edges.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Read about factories at https://github.com/thoughtbot/factory_girl

FactoryGirl.define do
factory :problem_edge do
from_problem_id 1
to_problem_id 1
end
end
5 changes: 5 additions & 0 deletions spec/models/problem_edge_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
require 'spec_helper'

describe ProblemEdge do
pending "add some examples to (or delete) #{__FILE__}"
end