2020软工第二次结对作业
这个作业属于哪个课程 | |
---|---|
结对同学的博客链接、本作业博客链接、GitHub项目地址
标题 | 链接 |
---|---|
具体分工
学号姓名 | |
---|---|
PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 60 |
Estimate | 估计这个任务需要多少时间 | 180 | 180 |
Development | 开发 | 600 | 800 |
Analysis | 需求分析 (包括学习新技术) | 120 | 180 |
Design Spec | 生成设计文档 | 20 | 20 |
Design Review | 设计复审 | 20 | 30 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 10 | 15 |
Design | 具体设计 | 30 | 60 |
Coding | 具体编码 | 520 | 680 |
Code Review | 代码复审 | 30 | 30 |
Test | 测试(自我测试,修改代码,提交修改) | 120 | 480 |
Reporting | 报告 | 10 | 20 |
Test Repor | 测试报告 | 10 | 20 |
Size Measurement | 计算工作量 | 10 | 15 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 30 | 30 |
合计 | 1740 | 2620 |
解题思路描述与设计实现说明
关键实现的流程图
代码实现思路文字描述以及代码展示
-
解析输入数据转换成JSON
1.0 定义ROOT = {};用于存储提取出来的JSON
1.1 通过两个空行分割每一组数据,从每一组数据的第一行提取出导师名字,完成后:
ROOT = { '导师1':{}, '导师2':{} }
1.2 遍历每一组数据,利用换行符来分割每一行数据,通过'级博士生'、'级硕士生'、'级本科生'等关键词来提取每一个导师所带过的年级,完成后:
ROOT = { '导师1':{'xxxx级博士生':{},'xxxx级本科生':{}}, '导师2':{'xxxx级博士生':{},'xxxx级硕士生':{}} }
1.3 遍历每一组年级数据,利用关键字'、'来分割每一位学生,完成后:
ROOT = { '导师1':{'xxxx级博士生':{'xxx':"",'xxx':"",'xx':""},'xxxx级本科生':{'xx':"",'xx':""}}, '导师2':{'xxxx级博士生':{'xxx':"",'xxx':"",'xx':""},'xxxx级硕士生':{'xx':"",'xx':""}} }
1.4 不包含'导师:'、'级xx生'等关键词的数据,通过关键词':'来提取技能树或者任职经历,完成后:
ROOT = { '导师1':{ 'xxxx级博士生':{'xxx':"京东云、阿里云、腾讯云",'xxx':"",'xx':""}, 'xxxx级本科生':{'xx':"",'xx':""} }, '导师2':{ 'xxxx级博士生':{'xxx':"",'xxx':"",'xx':"字节跳动、拼多多"}, 'xxxx级硕士生':{'xx':"",'xx':""} } }
代码展示
function analyse(){ var text = document.getElementById("text").value; var root = text.split(/\n\n\n/); $(".button").show(); for(var i in root){ var leader = root[i].split(/\n/); var teacher = leader[0].substring(3); ROOT[teacher] = {}; var grades = []; var re1=/[0-9]+\u7ea7\u672c\u79d1\u751f/; var re2=/[0-9]+\u7ea7\u535a\u58eb\u751f/; var re3=/[0-9]+\u7ea7\u7855\u58eb\u751f/; var re4 = /\u5bfc\u5e08/; for(var k in leader){ if(re1.exec(leader[k])){ grades.push(re1.exec(leader[k])); ROOT[teacher][re1.exec(leader[k])] = {}; for(var q in leader[k].substring(9).split(/\、/)){ ROOT[teacher][re1.exec(leader[k])][leader[k].substring(9).split(/\、/)[q]] = ""; } } if(re2.exec(leader[k])){ grades.push(re2.exec(leader[k])); ROOT[teacher][re2.exec(leader[k])] = {}; for(var q in leader[k].substring(9).split(/\、/)){ ROOT[teacher][re2.exec(leader[k])][leader[k].substring(9).split(/\、/)[q]] = ""; } } if(re3.exec(leader[k])){ grades.push(re3.exec(leader[k])); ROOT[teacher][re3.exec(leader[k])] = {}; for(var q in leader[k].substring(9).split(/\、/)){ ROOT[teacher][re3.exec(leader[k])][leader[k].substring(9).split(/\、/)[q]] = ""; } } if(!re1.exec(leader[k]) && !re2.exec(leader[k]) && !re3.exec(leader[k]) && !re4.exec(leader[k]) && leader[k]!=''){ for(var w in ROOT[teacher]){ for(var e in ROOT[teacher][w]){ if(e == leader[k].split(/\:/)[0]){ ROOT[teacher][w][e] = leader[k].split(/\:/)[1]; } } } } } } // console.log(ROOT); }
-
通过JSON来绘制师门树图
2.1 根据遍历
ROOT
的节点来画出导师节点2.2 根据遍历
ROOT['导师']
的节点来画出每一位导师所带过的年级节点2.3 根据遍历
ROOT['导师']['年级']
的节点来画出每一个年级的同学节点2.4 根据遍历所有同学节点,将同一个同学连接在一起形成关联树
代码展示
function createTree(){ var paras = document.getElementsByClassName('index'); while(paras[0]){ paras[0].parentNode.removeChild(paras[0]); } var svg = d3.select('body').append('svg').attr('width',window.innerWidth).attr('height',window.innerHeight); var g = svg.append('g'); var i = 1; for(var q in ROOT){ var num1 = Object.keys(ROOT).length; var x1 = i*window.innerWidth/(num1+1); var y1 = window.innerHeight/2; var j = 0; var rand1 = randomNum(0,90); for(var w in ROOT[q]){ var num2 = Object.keys(ROOT[q]).length; var angle = 360/(num2); var x2 = x1 + 150*Math.cos((angle*j+rand1)*Math.PI/180); var y2 = y1 + 150*Math.sin((angle*j+rand1)*Math.PI/180); var k = 0; var rand2 = randomNum(0,90); for(var e in ROOT[q][w]){ var num3 = Object.keys(ROOT[q][w]).length; var angle2 = 360/(num3); var x3 = x2 + 50*Math.cos((angle2*k+rand2)*Math.PI/180); var y3 = y2 + 50*Math.sin((angle2*k+rand2)*Math.PI/180); createline(x2,y2,x3,y3,'red','1px',q+w,q); if(ROOT[q][w][e]!=''){ createcircle(x3,y3,30,e,'SkyBlue','15px',q+w,q,1); } else{ createcircle(x3,y3,30,e,'SkyBlue','15px',q+w,q,1); } k++; } createcircle(x2,y2,25,w,'Orchid','7px',q+q); j++; } createcircle(x1,y1,50,q,'Gold','30px','root'); i++; } linksame(); }
附加特点设计与展示
一、支持修改同学的技能树或者工作经历并保存
-
设计的创意独到之处,这个设计的意义
方便在生成树之后再进行修改,不需要重新生成树
-
实现思路
将每一个同学的技能树通过文本输入框显示,同时增加保存按钮,在修改之后点击保存按钮即可保存
-
重要的代码片段
function detail(name){ NAME = name; for(var q in ROOT){ for(var w in ROOT[q]){ for(var e in ROOT[q][w]){ if(e==name){ document.getElementById("detail").value = ROOT[q][w][e]; } } } } $("#abouts").fadeToggle("slow"); } function initdetail(){ $(document).ready(function(){ $("#close").click(function(){ $("#abouts").fadeToggle("slow"); }); $("#save").click(function(){ var new_detail = document.getElementById("detail").value; for(var q in ROOT){ for(var w in ROOT[q]){ for(var e in ROOT[q][w]){ if(e==NAME){ ROOT[q][w][e] = new_detail; } } } } $("#abouts").fadeToggle("slow"); }); }); }
-
实现成果展示
二、支持上传TXT文件作为文本输入
-
设计的创意独到之处,这个设计的意义
方便用户直接通过文件导入,无需输入数据
-
实现思路
设置一个文件上传点,上传文件后根据文件内容改变文本框的值,进行接下来的操作
-
重要的代码片段
function fileUpload() { let file = document.getElementById('fileinp').files[0]; let reader = new FileReader(); reader.readAsText(file, 'utf-8'); console.log(reader.result); reader.onload = function () { document.getElementById("text").value = reader.result; analyse(); } }
-
实现成果展示
三、支持将生成的师门树导出为图片
-
设计的创意独到之处,这个设计的意义
方便用户在其它场景使用生成的师门树,不局限于浏览器
-
实现思路
将所绘制出的SVG图片下载到用户电脑
-
重要的代码片段
function save(){ var serializer = new XMLSerializer(); var svg1 = document.querySelector('svg'); var toExport = svg1.cloneNode(true); var bb = svg1.getBBox(); toExport.setAttribute('viewBox', bb.x + ' ' + bb.y + ' ' + bb.width + ' ' + bb.height); toExport.setAttribute('width', bb.width); toExport.setAttribute('height', bb.height); var source = '<?xml version="1.0" standalone="no"?>\r\n' + serializer.serializeToString(toExport); var image = new Image; image.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(source); var canvas = document.createElement("canvas"); canvas.width = bb.width; canvas.height = bb.height; var context = canvas.getContext("2d"); context.fillStyle = '#fff';//#fff设置保存后的PNG 是白色的 context.fillRect(0, 0, 10000, 10000); image.onload = function(){ context.drawImage(image, 0, 0); var a = document.createElement("a"); a.download = "tree.png"; a.href = canvas.toDataURL("image/png"); a.click(); } }
-
实现成果展示
四、树的绘制函数全为自主编写,因此风格独特并且支持多棵关联树并存
-
设计的创意独到之处,这个设计的意义
页面设计比平常所见的模板更为独特,同时支持多棵关联树的存在,使得师门树更加清晰明了
-
实现思路
页面设计的思路为,用不同颜色代表每个节点的所属关系,节点利用大小不同的圆绘制而成
关联树的实现思路为,遍历所有屏幕前显示的学生节点,将同一位学生的多个节点用曲线连接起来,以此实现关联树
-
重要的代码片段
一、树的绘制函数:
function createTree(){ var paras = document.getElementsByClassName('index'); while(paras[0]){ paras[0].parentNode.removeChild(paras[0]); } var svg = d3.select('body').append('svg').attr('width',window.innerWidth).attr('height',window.innerHeight); var g = svg.append('g'); var i = 1; for(var q in ROOT){ var num1 = Object.keys(ROOT).length; var x1 = i*window.innerWidth/(num1+1); var y1 = window.innerHeight/2; var j = 0; var rand1 = randomNum(0,90); for(var w in ROOT[q]){ var num2 = Object.keys(ROOT[q]).length; var angle = 360/(num2); var x2 = x1 + 150*Math.cos((angle*j+rand1)*Math.PI/180); var y2 = y1 + 150*Math.sin((angle*j+rand1)*Math.PI/180); //createline(x1,y1,x2,y2,'blue','3px',q+q); var k = 0; var rand2 = randomNum(0,90); for(var e in ROOT[q][w]){ var num3 = Object.keys(ROOT[q][w]).length; // console.log(num3); var angle2 = 360/(num3); var x3 = x2 + 50*Math.cos((angle2*k+rand2)*Math.PI/180); var y3 = y2 + 50*Math.sin((angle2*k+rand2)*Math.PI/180); // console.log(80*Math.cos(angle2*k*Math.PI/180)+" "+80*Math.sin(angle2*k*Math.PI/180)+" "+k); createline(x2,y2,x3,y3,'red','1px',q+w,q); if(ROOT[q][w][e]!=''){ createcircle(x3,y3,30,e,'SkyBlue','15px',q+w,q,1); } else{ createcircle(x3,y3,30,e,'SkyBlue','15px',q+w,q,1); } k++; } createcircle(x2,y2,25,w,'Orchid','7px',q+q); j++; } createcircle(x1,y1,50,q,'Gold','30px','root'); i++; } linksame(); }
二、多棵关联树并存的实现:
function linksame(){ var svg = d3.select("svg"); var g = svg.append('g'); for(var i in ROOT){ for(var j in ROOT[i]){ for(var k in ROOT[i][j]){ var point = $("[view='"+k+"']"); var result = []; for(var q=0;q<point.length;q++){ classs = point.eq(q).attr("class"); $('.'+classs).toggle(); $('.'+classs).toggle(); if($('.'+classs).attr("style")=="display: inline;"){ result.push(point.eq(q).attr("xy")); } } point = result; for(var a=0;a<point.length;a++){ for(var b=0;b<point.length;b++){ var x1 = point[a].split(/\,/)[0]; var y1 = point[a].split(/\,/)[1]; var x2 = point[b].split(/\,/)[0]; var y2 = point[b].split(/\,/)[1]; quxian(parseInt(x1),parseInt(y1),parseInt(x2),parseInt(y2)); } } } } } }
-
实现成果展示
目录说明和使用说明
目录说明
├── static
│ ├── css
├──index.css //主CSS代码
│ ├── images //存放背景图片
│ └── js
│ ├──d3.js //d3库
│ ├──index.js //主JS代码
│ └──jquery-1.11.0.min.js //jquery库
└── index.html //入口文件
使用说明
- 从github仓库内下载项目到本地,解压到文件夹,用chrome打开文件夹中www目录下的
index.html
即可运行。
测试数据格式要求(请务必严格按照格式输入)
导师:张三 (冒号为中文冒号)(导师名字不要重复)
2016级博士生:天一、王二、吴五 (多个学生间以顿号间隔区分)
2015级硕士生:李四、王五、许六 (多行数据间以换行区分)
2016级硕士生:刘一、李二、李三
2017级本科生:刘六、琪七、司四
刘六:JAVA、数学建模
李二:字节跳动、京东云
(这里是第一个空行)
(这里是第二个空行) (多组数据间间隔两行并严格按照上述数据格式)
导师:王导 (冒号为中文冒号)(导师名字不要重复)
2016级博士生:天一1、王二2、吴五5 (多个学生间以顿号间隔区分)
2015级硕士生:李四4、王五5、许六6 (多行数据间以换行区分)
2016级硕士生:刘一1、李二2、李三
2017级本科生:刘六、琪七7、司四4
刘六:JAVA、数学建模
李二2:字节跳动、京东云
单元测试
测试工具及简易教程
测试工具使用的是mocha,使用前需要搭建好node.js和npm环境,配置mocha环境主要分为两种,全局配置和当前目录配置,为了方便起见,在配置的时候我们选择了在当前目录和全局内都进行配置的方法。参考的博客有:http://www.ruanyifeng.com/blog/2015/12/a-mocha-tutorial-of-examples.html和廖雪峰的https://www.liaoxuefeng.com/wiki/1022910821149312/1101756368943712 。
单元测试代码
构建测试数据思路
列举几种在输入文本时容易出现的文本格式错误形式,对代码中的主要函数进行测试。
GitHub的代码迁入记录截图
遇到的代码模块异常及解决方法
一、节点缩放异常
-
问题描述
因为是自己写的绘制函数,导致缩放功能也得自己实现,在缩放时,有A(B(C))三类节点,当C类节点被隐藏时,此时缩放A节点,将导致C节点的缩放异常。例:年级节点被收缩起来时,学生节点却仍然存在在屏幕中
-
做过哪些尝试
在缩放导师节点之后,将改导师的所有子节点全部隐藏而不是改变是否显示在屏幕上
-
是否解决
已解决
-
有何收获
在思考问题时,要将代码逻辑理清楚,这样就可以很快速的找到问题所在和解决方法
二、关联树实现异常
-
问题描述
同样因为需要自己实现,因此关联树代码写起来有点吃力
首先第一个遇到的困难是,通过d3选择器获取的节点属性值无法进行遍历,导致无法获取每一个节点的x和y值
其次,关联曲线没有办法根据节点的缩放而缩放,会一直存在屏幕上
-
做过哪些尝试
当d3选择器无法进行遍历时,采用多个选择器联动,来获取一个可以遍历的对象
当关联曲线没有办法缩放时,采用动态绘制的方法,每一次进行节点缩放是,擦除所有已经画出来的关联曲线,根据当前在屏幕上有显示的节点再绘制关联曲线
-
是否解决
已解决
-
有何收获
- 在d3选择器所获得的属性值无法进行遍历时,可以采用JS选择器,JQUERY选择器,D3选择器联动实现
- 要从多个角度思考解决问题
- 学会了各种调试姿势
三、技能树修改异常
-
问题描述
技能树查看时,为进行修改就点击保存会导致技能书内容被清空,同时click函数会被触发两次导致
-
做过哪些尝试
尝试利用FLAG带标志触发的次数,以此来避免第二次的触发
最终发现是因为每一个节点的click函数被多次定义导致运行出错,修改后即可正确运行
-
是否解决
已解决
-
有何收获
记住了不能在同一个地方多次定义jquery选择器的click函数,将会导致代码出现奇奇怪怪的错误
四、.........
- 在代码的具体编写过程中,难免遇到了各种模块特性、代码逻辑等等各种问题所导致的异常,经常一个问题困扰了我一整个晚上,但是在各种资料收集以及缕清思路之后,总能得到解决。因此学到了以后遇到异常时不要钻牛角尖,学会从其它角度看代和思考问题。
评价你的队友
陈凯强
- 值得学习的地方:在协作中心态良好,无论bug有多少都能够耐心修改,在code过程中有时间观念。
- 需要改进的地方:自学能力不够出色,面对一些困难时经常没有解决思路,有点直男审美。
陈龙辉
- 值得学习的地方:执行编码能力强大,自学能力极强;能肝,好几次code到凌晨两点;思维灵活性强,在代码过程中遇到奇怪的bug积极面向浏览器编程查找答案,在众多奇怪的代码中筛选出可以使用的方案。
- 需要改进的地方:编码时变量名的设置仍需改进,部分代码阅读感不够强。