From c2566bbc147e1161b2a0511b590346d9d47f5369 Mon Sep 17 00:00:00 2001 From: Kento HASEGAWA Date: Thu, 25 Jul 2019 01:38:34 +0900 Subject: [PATCH] Add solution viewer --- main.py | 4 + roles/host.py | 20 ++ static/js/adc2019-viewer.js | 377 +++++++++++++++++++++++++++++++++++ static/js/adc2019.js | 6 +- templates/view-solution.html | 49 +++++ utils/data.py | 95 ++++++++- 6 files changed, 546 insertions(+), 5 deletions(-) create mode 100644 static/js/adc2019-viewer.js create mode 100644 templates/view-solution.html diff --git a/main.py b/main.py index e222197..f8feb4d 100644 --- a/main.py +++ b/main.py @@ -16,6 +16,10 @@ def webui_index(): # return adc2019system.role.role return render_template('index.html') +@webui.route('/view/solution') +def webui_view_solution(): + return render_template('view-solution.html') + @webui.route('/part/problems') def webui_part_problems(): if (adc2019system.role is not None) and (adc2019system.role.type == 'host'): diff --git a/roles/host.py b/roles/host.py index eabfc42..400fce4 100644 --- a/roles/host.py +++ b/roles/host.py @@ -94,6 +94,22 @@ class Host(object): return self.request[request_id].get_status() else: return {'status': 'unknown request'} + + def get_solution_for_viewer(self, problem_key, solution_id): + problem = self.get_problem(problem_key) + if problem is None: + return None + problem_data = problem.get_d3json() + + solution = problem.get_solution(solution_id) + if solution is None: + return None + solution_data = solution.get_d3json() + + return { + 'problem': problem_data, + 'solution': solution_data + } def call_api(self, method, cmd, params): if cmd == 'role': @@ -109,6 +125,10 @@ class Host(object): elif cmd == 'request/status': request_id = float(params['request_id']) return self.get_request_status(request_id) + elif cmd == 'view/solution': + problem_key = params['problem'] + solution_id = params['solution'] + return self.get_solution_for_viewer(problem_key, solution_id) else: return None diff --git a/static/js/adc2019-viewer.js b/static/js/adc2019-viewer.js new file mode 100644 index 0000000..f991fd0 --- /dev/null +++ b/static/js/adc2019-viewer.js @@ -0,0 +1,377 @@ + +class ADC2019BoardViewer { + + constructor(selector) { + this.container = $(selector); + this.width = this.container.width(); + this.height = $(window).height(); + + this.svg = d3.select(selector).append('svg'); + this.view = this.svg.append('g').attr('class', 'view'); + + this.currentTransform = null; + + this.cubeResolution = 50; + + this._init(); + this._draw_slider(); + } + + draw(data){ + this._draw_init(); + // this._get_board(data); + + this._draw_grid(data['problem']['w'], data['problem']['h']); + this._draw_solution(data); + } + + _init(){ + var _self = this; + + if (this.currentTransform){ + this.view.attr('transform', this.currentTransform); + } + } + + _draw_init(){ + this.view.selectAll("g").remove(); + } + + _draw_grid(x_cnt, y_cnt){ + let _width = x_cnt * this.cubeResolution; + let _height = y_cnt * this.cubeResolution; + this.view.append("g") + .attr("class", "x axis") + .selectAll("line") + .data(d3.range(0, _width+1, this.cubeResolution)) + .enter().append("line") + .attr("x1", function (d) { return d; }) + .attr("y1", 0) + .attr("x2", function (d) { return d; }) + .attr("y2", _height) + .attr("stroke", "#888"); + + this.view.append("g") + .attr("class", "y axis") + .selectAll("line") + .data(d3.range(0, _height+1, this.cubeResolution)) + .enter().append("line") + .attr("x1", 0) + .attr("y1", function (d) { return d; }) + .attr("x2", _width) + .attr("y2", function (d) { return d; }) + .attr("stroke", "#888"); + } + + _draw_slider(){ + + let _self = this; + + var zoom = d3.zoom() + .scaleExtent([0.5, 5]) + .translateExtent([ + [-this.width * 2, -this.height * 2], + [this.width * 2, this.height * 2] + ]) + .on("zoom", zoomed); + + function zoomed() { + this.currentTransform = d3.event.transform; + _self.view.attr("transform", this.currentTransform); + // this.slider.property("value", d3.event.scale); + } + + function slided(d) { + zoom.scaleTo(this.svg, d3.select(this).property("value")); + } + + // var slider = d3.select("body").append("input") + // .datum({}) + // .attr("type", "range") + // .attr("value", 1) + // .attr("min", zoom.scaleExtent()[0]) + // .attr("max", zoom.scaleExtent()[1]) + // .attr("step", (zoom.scaleExtent()[1] - zoom.scaleExtent()[0]) / 100) + // .on("input", slided); + + this.svg.call(zoom).on('dblclick.zoom', null); + + } + + _draw_solution(data){ + + let cubeResolution = this.cubeResolution; + + // Draw outer box + let _width = data['solution']['w'] * this.cubeResolution; + let _height = data['solution']['h'] * this.cubeResolution; + this.view.append("g") + .attr("class", "x axis") + .selectAll("line") + .data([0, _width]) + .enter().append("line") + .attr("x1", function (d) { return d; }) + .attr("y1", 0) + .attr("x2", function (d) { return d; }) + .attr("y2", _height) + .attr("stroke", "#d00") + .attr("stroke-width", 2); + + this.view.append("g") + .attr("class", "y axis") + .selectAll("line") + .data([0, _height]) + .enter().append("line") + .attr("x1", 0) + .attr("y1", function (d) { return d; }) + .attr("x2", _width) + .attr("y2", function (d) { return d; }) + .attr("stroke", "#d00") + .attr("stroke-width", 2); + + + let colors = d3.schemeCategory10 + + // Reference + // Grid: https://bl.ocks.org/ngminhtrung/7c5721a1504f3e29a36da9ddd9e5039b + // Snap: https://bl.ocks.org/evanjmg/ea3e59e67b4256c8831d3fc80f71294b + // Nested data structure: https://codeday.me/jp/qa/20190428/720184.html + + // Create data + var blocks = data['problem']['block']; + for(var bi in blocks){ + let bw = blocks[bi]['w']; + let bh = blocks[bi]['h']; + let bx = data['solution']['block'][bi]['x']; + let by = data['solution']['block'][bi]['y']; + + blocks[bi]['cellpos'] = Array(); + for(var _h=0; _h 'translate(' + d.x + ',' + d.y + ')') + .attr('data-x', (d) => d.x) + .attr('data-y', (d) => d.y); + + var cellContainer = itemContainer.append('g') + .attr('class', 'cellContainer') + .attr('data-color', (d, i) => colors[i]) + .attr('x', 0) + .attr('y', 0) + + cellContainer.selectAll('g') + .data((d) => d.cellpos) + .enter() + .append('rect') + .attr('x', (d) => d[0] * this.cubeResolution) + .attr('y', (d) => d[1] * this.cubeResolution) + .attr('width', this.cubeResolution) + .attr('height', this.cubeResolution) + .attr('cursor', 'move') + .attr('fill', (d, i, nodes) => { + return d3.select(nodes[i].parentNode).attr('data-color'); + }); + + cellContainer.selectAll('g') + .data((d) => d.cellpos) + .enter() + .append('text') + .attr('x', (d) => d[0] * this.cubeResolution + 0.5 * this.cubeResolution) + .attr('y', (d) => d[1] * this.cubeResolution + 0.5 * this.cubeResolution) + .attr('width', this.cubeResolution) + .attr('height', this.cubeResolution) + .attr('text-anchor', 'middle') + .attr('fill', 'white') + .attr('font-size', '20px') + .attr('cursor', 'move'); + // .text((d) => d[2]); + + let sw = data['solution']['w']; + let sh = data['solution']['h']; + var lines = Object(); + for(var _h=0; _h lineColors[i]); + + lineContainer.selectAll('g') + .data((d) => d) + .enter() + .append('rect') + .attr('x', (d) => (d[0]+0.25) * this.cubeResolution) + .attr('y', (d) => (d[1]+0.25) * this.cubeResolution) + .attr('width', (this.cubeResolution*0.5)) + .attr('height', (this.cubeResolution*0.5)) + .attr('fill', (d, i, nodes) => { + return d3.select(nodes[i].parentNode).attr('data-color'); + }); + + lineContainer.selectAll('g') + .data((d) => d) + .enter() + .append('text') + .attr('x', (d) => d[0] * this.cubeResolution + 0.5 * this.cubeResolution) + .attr('y', (d) => d[1] * this.cubeResolution + 0.5 * this.cubeResolution) + .attr('width', (this.cubeResolution*0.5)) + .attr('height', (this.cubeResolution*0.5)) + .attr('text-anchor', 'middle') + .attr('fill', 'black') + .attr('font-size', '16px') + .text((d) => d[2]); + + } + // _draw_blocks(blocks){ + + // let cubeResolution = this.cubeResolution; + + // function snapToGrid(p, r) { + // return Math.round(p / r) * r; + // } + + // function dragged(d) { + // var el = d3.select(this); + // var _x = parseInt(el.attr('data-x'), 10) + d3.event.x; + // var _y = parseInt(el.attr('data-y'), 10) + d3.event.y; + // el.attr("transform", (d) => { + // return 'translate(' + snapToGrid(_x, cubeResolution) + ',' + snapToGrid(_y, cubeResolution) + ')' + // }) + // } + // function dragended(d) { + // var el = d3.select(this).classed("dragging", false); + // var _x = parseInt(el.attr('data-x'), 10) + d3.event.x; + // var _y = parseInt(el.attr('data-y'), 10) + d3.event.y; + // d3.select(this) + // .attr('data-x', snapToGrid(_x, cubeResolution)) + // .attr('data-y', snapToGrid(_y, cubeResolution)); + // } + // function dragstarted(d) { + // var el = d3.select(this); + // el.raise().classed("dragging", true); + // } + + // let colors = d3.schemeCategory10 + + // // Reference + // // Grid: https://bl.ocks.org/ngminhtrung/7c5721a1504f3e29a36da9ddd9e5039b + // // Snap: https://bl.ocks.org/evanjmg/ea3e59e67b4256c8831d3fc80f71294b + // // Nested data structure: https://codeday.me/jp/qa/20190428/720184.html + + // var itemContainer = this.view.selectAll("g.itemContainer") + // .data(blocks) + // .enter() + // .append('g') + // .attr('class', 'itemContainer') + // .attr("transform", (d) => 'translate(' + d.x + ',' + d.y + ')') + // .attr('data-x', (d) => d.x) + // .attr('data-y', (d) => d.y) + // .call(d3.drag() + // .on("start", dragstarted) + // .on("drag", dragged) + // .on("end", dragended)); + + // var cellContainer = itemContainer.append('g') + // .attr('class', 'cellContainer') + // .attr('data-color', (d, i) => colors[i]) + // .attr('x', 0) + // .attr('y', 0) + + // cellContainer.selectAll('g') + // .data((d) => d.cells) + // .enter() + // .append('rect') + // .attr('x', (d) => d[0] * this.cubeResolution) + // .attr('y', (d) => d[1] * this.cubeResolution) + // .attr('width', this.cubeResolution) + // .attr('height', this.cubeResolution) + // .attr('cursor', 'move') + // .attr('fill', (d, i, nodes) => { + // return d3.select(nodes[i].parentNode).attr('data-color'); + // }); + + // cellContainer.selectAll('g') + // .data((d) => d.cells) + // .enter() + // .append('text') + // .attr('x', (d) => d[0] * this.cubeResolution + 0.5 * this.cubeResolution) + // .attr('y', (d) => d[1] * this.cubeResolution + 0.5 * this.cubeResolution) + // .attr('width', this.cubeResolution) + // .attr('height', this.cubeResolution) + // .attr('text-anchor', 'middle') + // .attr('fill', 'white') + // .attr('font-size', '20px') + // .attr('cursor', 'move') + // .text((d) => d[2]); + + // } +} + +$(function(){ + + const board = new ADC2019BoardViewer("#board-container"); + + hash = location.hash.replace("#", ""); + params = hash.split('/'); + + $.ajax({ + type: "POST", + dataType: "json", + url: "/api/view/solution", + data: JSON.stringify({ + "problem": params[0], + "solution": params[1] + }), + contentType: 'application/json' + }).done((d) => { + board.draw(d); + }); + +}); + diff --git a/static/js/adc2019.js b/static/js/adc2019.js index f0d0365..171cbb2 100644 --- a/static/js/adc2019.js +++ b/static/js/adc2019.js @@ -30,7 +30,7 @@ class StatusView { _this.container.find('.solution-detail-row td').click((e) => { var solution_id = $(e.target).parent("tr").data("solution-id"); var problem_name = $(e.target).parent("tr").data("problem"); - var viewer_url = "/viewer/solution#" + problem_name + "/" + solution_id; + var viewer_url = "/view/solution#" + problem_name + "/" + solution_id; window.open(viewer_url, "_blank"); }) }); @@ -59,7 +59,7 @@ class StatusView { show: true }); _this.container.find('#solver-processing-modal').on('hidden.bs.modal', function(e){ - console.log(e); + // console.log(e); _this.show_problem(); }) @@ -78,7 +78,7 @@ class StatusView { _this.container.find('#request-status-container').html(d); status = $(d).find('#request-status-value').text(); - console.log(status); + // console.log(status); if(status == 'done'){ _this.request_refresh_timer = null; }else{ diff --git a/templates/view-solution.html b/templates/view-solution.html new file mode 100644 index 0000000..f9f3908 --- /dev/null +++ b/templates/view-solution.html @@ -0,0 +1,49 @@ + + + + ADC2019 Solver System + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ + +
+ + + diff --git a/utils/data.py b/utils/data.py index feaad10..e83a6f6 100644 --- a/utils/data.py +++ b/utils/data.py @@ -1,6 +1,7 @@ import datetime import json import os +import re import time import uuid @@ -12,6 +13,7 @@ class Problem(object): self.name = '' self.size = (0, 0) self.block_num = 0 + self.blocks = dict() self.problem = '' self.status = 'Ready' self.solutions = dict() @@ -27,22 +29,51 @@ class Problem(object): with open(path, 'r') as fp: q_text = fp.read() - q_lines = fp.readlines() + q_lines = q_text.splitlines() + board_size = [0, 0] block_num = 0 + blocks = dict() + + def intplus(v): + if v.isdecimal(): + return int(v) + else: + return v - for _l in q_lines: + li = 0 + while li < len(q_lines): + _l = q_lines[li] if "SIZE" in _l: board_size_str = _l.strip().split()[1] board_size = [int(v) for v in board_size_str.split('X')] if 'BLOCK_NUM' in _l: block_num = int(_l.strip().split()[1]) + if 'BLOCK#' in _l: + p = r'BLOCK#([0-9]+) +([0-9]+)X([0-9]+)' + m = re.match(p, _l.strip()) + bi = int(m.group(1)) + bw = int(m.group(2)) + bh = int(m.group(3)) + blocks[bi] = { + 'index': bi, + 'w': bw, + 'h': bh, + 'cells': list() + } + for _h in range(bh): + li += 1 + _l = q_lines[li].strip() + blocks[bi]['cells'].append([intplus(v.strip()) for v in _l.split(',')]) + + li += 1 name = os.path.splitext(os.path.basename(path))[0] self.size = board_size self.block_num = block_num + self.blocks = blocks self.name = name self.problem = q_text self.status = 'Ready' @@ -56,6 +87,15 @@ class Problem(object): 'problem': self.problem, 'status': self.status } + + def get_d3json(self): + + return { + 'block': self.blocks, + 'w': self.size[0], + 'h': self.size[1], + 'n': self.block_num + } def put_solution(self, data): solution = Solution(data) @@ -68,6 +108,12 @@ class Problem(object): def get_solutions(self): return self.solutions + + def get_solution(self, solution_id): + if solution_id in self.solutions: + return self.solutions[solution_id] + else: + return None class Solution(object): @@ -96,6 +142,51 @@ class Solution(object): 'solution': self.solution } + def get_d3json(self): + + board_size = [0, 0] + block_num = 0 + + _lines = self.solution.splitlines() + + li = 0 + + bw, bh = 0, 0 + bmap = list() + bposition = dict() + + while li < len(_lines): + _l = _lines[li].strip() + if 'SIZE' in _l: + board_size_str = _l.strip().split()[1] + board_solution_size = [int(v) for v in board_size_str.split('X')] + bw = board_solution_size[0] + bh = board_solution_size[1] + for _h in range(bh): + li += 1 + _l = _lines[li].strip() + bmap.append([int(v.strip()) for v in _l.split(',')]) + if 'BLOCK' in _l: + p = r'BLOCK#([0-9]+) +@\(([0-9]+), *([0-9]+)\)' + m = re.match(p, _l.strip()) + bi = int(m.group(1)) + bx = int(m.group(2)) + by = int(m.group(3)) + bposition[bi] = { + 'index': bi, + 'x': bx, + 'y': by, + } + + li += 1 + + return { + 'w': bw, + 'h': bh, + 'map': bmap, + 'block': bposition + } + def save(self, basedir): outdir = f"{basedir}/{self.problem}" if not os.path.exists(outdir): -- 2.22.0