结对项目作业——求交点

项目 内容
这个作业属于哪个课程 2020春季计算机学院软件工程(罗杰 任健)
这个作业的要求在哪里 结对项目作业
我在这个课程的目标是 学习软工的思想方法,写出好的软件并维护
这个作业在哪个具体方面帮助我实现目标 学习了如何封装core,如何编写动态库并在另一个项目中引用,同时初步接触结对编程,认识到了这种开发方式的优缺点。
班级 006
项目地址(可以看到提交记录以及tag的地址) githubProcess
项目地址(最终的可运行版本,方便助教clone) githubFinal

一、PSP时间记录

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划
· Estimate · 估计这个任务需要多少时间 1620 1760
Development 开发
· Analysis · 需求分析 (包括学习新技术) 800 800
· Design Spec · 生成设计文档 90 120
· Design Review · 设计复审 (和同事审核设计文档) 10 10
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 40 60
· Design · 具体设计 40 80
· Coding · 具体编码 400 500
· Code Review · 代码复审 20 30
· Test · 测试(自我测试,修改代码,提交修改) 60 120
Reporting 报告
· Test Report · 测试报告 30 15
· Size Measurement · 计算工作量 30 20
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 20 5
合计 1620 1760

二、扩展计算交点功能

1.新增功能介绍

本次新增的功能为支持线段和射线这种几何体加入到交点的计算中,下面来分析加入这两个新的几何体后,程序需要做的改变。

首先,在读入一个几何体的数据后,先判断是什么,如果是射线,它的方程和直线是一样的,记做$$ Ax+By+c=0 $$

除此以外,记录其发射点为(x1, y1),同时记录射线的射出方向dirct,这里有四个方向:向左,向右,垂直向上,垂直向下;如果是线段,方程也记为:$$ Ax+By+c=0 $$,同时记录其两个顶点分别为(x1, y1) , (x2, y2);其他的直线和圆处理同上一次,不再赘述。

之后就是计算的过程了,大体的思路还是一样的,几何体间两两求交点,首先是线与线之间,先联立两条线的方程,如果是平行的直接返回,如果有交点,则要判断这个交点是否在两条线上。如果是两条直线,那么一定在,将其加入set中,如果是一条直线和一条射线,只需判断求出的点是否在射线上即可,这里用到函数is_on_ray(Line l, crosspoint point),如下是关键代码:

if (l.dirct == 0) {
    if (point.x >= l.x1) {
        return true;
    }
    else {
        return false;
    }
}
else if (l.dirct == 1) {
    if (point.x <= l.x1) {
        return true;
    }
    else {
        return false;
    }
}
else if (l.dirct == 2) {
    if (point.y >= l.y1) {
        return true;
    }
    else {
        return false;
    }
}
else {
    if (point.y <= l.y1) {
        return true;
    }
    else {
        return false;
    }
}

思路大体如下,如果射线的方向向右,那么这个求出的交点的横坐标只要大于等于射线的顶点的横坐标即可,同理射线向右时交点横坐标要小于等于顶点的横坐标,向下时交点的纵坐标要小于等于顶点的纵坐标,向上时交点的纵坐标要大于等于顶点的纵坐标。

通过这个函数我们可以判断交点是否在射线上。

继续上文,若是两条线是一条直线和一条线段,那么只需要判断交点是否在线段上即可,这里用到另一个函数is_on_segment(Line l, crosspoint point),代码如下:

bool is_on_segment(Line l, crosspoint point)
{//直线和线段的交点问题
    if (l.x1 == l.x2) {
        if (point.y >= l.y1 && point.y <= l.y2) {
            return true;
        }
        else {
            return false;
        }
    }
    else {
        if (point.x >= l.x1 && point.x <= l.x2) {
            return true;
        }
        else {
            return false;
        }
    }
}

这里的思想是,如果这条线段不是垂直于x轴的,那么交点的横坐标必须大于等于左顶点的横坐标且小于等于右顶点的横坐标,如果是垂直的,那么交点的纵坐标必须大于等于下顶点的纵坐标且小于等于上顶点的纵坐标。

通过这个函数我们可以判断交点是否在线段上。

继续上文,除了之前说的三种情况,还有射线与射线的交点不仅要在第一条射线上,还得在第二条射线上,射线与线段,线段与线段同理,不再赘述。

接下来是线与圆,其他的逻辑是一样的,首先当作直线和圆的交点做计算,求得交点之后如果是射线和线段,判断交点是否在射线和线段上,如果验证是交点则可以加入set。

下面是新的calculate_line_circle中的关键代码:

    double a = l.a * l.a + 1;
    double b = 2 * ((l.b - c.n) * l.a - c.m);
    double t = (double)c.m * c.m + (l.b - c.n) * (l.b - c.n) - (double)c.r * c.r;
    double deta = b * b - 4 * a * t;
    if (deta > 0) {
        point1.x = (sqrt(deta) - b) / (2 * a);
        point2.x = (-1 * sqrt(deta) - b) / (2 * a);
        point1.y = l.a * point1.x + l.b;
        point2.y = l.a * point2.x + l.b;
        if (l.type == 0) {
            Setpoint.insert(point1);
            Setpoint.insert(point2);
            return 2;
        }
        else if (l.type == 1) {
            int num = 0;
            if (is_on_ray(l, point1)) {
                Setpoint.insert(point1);
                num++;
            }
            if (is_on_ray(l, point2)) {
                Setpoint.insert(point2);
                num++;
            }
            return num;
        }
        else {
            int num = 0;
            if (is_on_segment(l, point1)) {
                Setpoint.insert(point1);
                num++;
            }
            if (is_on_segment(l, point2)) {
                Setpoint.insert(point2);
                num++;
            }
            return num;
        }
    }
    else if (deta == 0) {
        point1.x = (b == 0) ? 0 : -1 * b / (2 * a);
        point1.y = l.a * point1.x + l.b;
        if (l.type == 0) {
            Setpoint.insert(point1);
            return 1;
        }
        else if (l.type == 1) {
            if (is_on_ray(l, point1)) {
                Setpoint.insert(point1);
                return 1;
            }
            else {
                return 0;
            }
        }
        else {
            if (is_on_segment(l, point1)) {
                Setpoint.insert(point1);
                return 1;
            }
            else {
                return 0;
            }
        }
    }
    else {
        return 0;
    }

圆和圆的交点逻辑没有任何变化,不再赘述。

2.单元测试之新增功能

TEST_METHOD(calculate_line_line)
		{
			Intersect p;
			Line l1(0, 0, 1, 1, 0);
			Line l2(0, 0, 0, 1, 0);
			int ret = p.calculate_line_line(l1, l2);
			Assert::AreEqual(ret,(int)1);
			Line l3(0, 0, 1, 1, 0);
			Line l4(1, 0, 2, 1, 0);
			ret = p.calculate_line_line(l3, l4);
			Assert::AreEqual(ret, (int)0);
			Line l5(0, 0, 1, 0, 2);
			Line l6(1, 0, 2, 0, 2);
			ret = p.calculate_line_line(l5, l6);
			Assert::AreEqual(ret, (int)1);
			Line l7(0, 0, 1, 0, 2);
			Line l8(1, 1, 2, 0, 2);
			ret = p.calculate_line_line(l7, l8);
			Assert::AreEqual(ret, (int)0);
			Line l9(0, 0, 1, 1, 1);
			Line l10(0, 0, -1, -1, 1);
			ret = p.calculate_line_line(l9, l10);
			Assert::AreEqual(ret, (int)1);
			Line l11(0, 0, 1, 0, 1);
			Line l12(0, 1, 0, 2, 1);
			ret = p.calculate_line_line(l11, l12);
			Assert::AreEqual(ret, (int)0);
		}

这是测试calculate_line_line函数的单元测试,考虑了直线,射线,线段的组合情况,考虑两线平行的情况,特殊的是比如两射线不平行,但求出的交点不在射线上,也无交点的情况。

TEST_METHOD(calculate_line_circle)
		{
			Intersect p;
			Line l1(0, 0, 1, 1, 0);
			Circle c1(0, 1, 1);
			int ret = p.calculate_line_circle(l1, c1);
			Assert::AreEqual(ret, (int)2);
			Line l2(0, 0, 0, 1, 0);
			Circle c2(1, 0, 1);
			ret = p.calculate_line_circle(l2, c2);
			Assert::AreEqual(ret, (int)1);
			Line l3(0, 0, 1, 0, 0);
			Circle c3(0, 2, 1);
			ret = p.calculate_line_circle(l3, c3);
			Assert::AreEqual(ret, (int)0);
			Line l4(0, 1, 1, 1, 1);
			ret = p.calculate_line_circle(l4, c1);
			Assert::AreEqual(ret, (int)1);
			Line l5(0, 0, 0, 1, 2);
			Circle c4(0, 0, 3);
			ret = p.calculate_line_circle(l5, c4);
			Assert::AreEqual(ret, (int)0);
			Line l6(2, 1, 0, 1, 2);
			ret = p.calculate_line_circle(l4, c1);
			Assert::AreEqual(ret, (int)1);
		}

这是测试calculate_line_circle函数的单元测试,考虑了直线,射线,线段和圆交点的可能,其中直线考虑了和圆有一个,两个,无交点的三种情况,而射线和线段其实和直线的区别只是求出的交点可能不在线上,因此又单独测试了虽然可以通过方程求出交点,但不在线上的情况。

TEST_METHOD(calculate_circle_circle)
		{
			Intersect cal;
			Circle c1(0, 0, 1);
			Circle c2(1, 1, 1);
			int ret = cal.calculate_circle_circle(c1, c2);
			Assert::AreEqual(ret, (int)2);
			Circle c3(0, 0, 2);
			Circle c4(3, 0, 1);
			ret = cal.calculate_circle_circle(c3, c4);
			Assert::AreEqual(ret, (int)1);
			Circle c5(0, 0, 3);
			Circle c6(0, 0, 1);
			ret = cal.calculate_circle_circle(c5, c6);
			Assert::AreEqual(ret, (int)0);
			Circle c7(0, 0, 1);
			Circle c8(0, 9, 1);
			ret = cal.calculate_circle_circle(c7, c8);
			Assert::AreEqual(ret, (int)0);
		}

这是测试calculate_circle_circle函数的单元测试,考虑了两个圆有2个,1个,0个交点的情况,同时还考虑了一个圆在另一个圆内部的情况

可以看到测试通过。

三、将扩展后的功能封装为独立模块

1.看 Design by Contract,Code Contract 的内容,描述这些做法的优缺点,说明你是如何把它们融入结对作业中的。

在我的理解中,契约式设计的主要目的是希望程序员能够在设计程序时明确地规定一个模块单元在调用某个操作前后应当属于何种状态。DSB不是将程序员的编程思路限制住,而是一种语法的规范,是一种需要培养的好习惯。他是程序的调用者和被调用者处于同等的地位,双方具有同等的义务,调用者保证参数的正确性,被调用者保证结果的不变性,这就使得工程的不同阶段可以同步进行。
约式设计强调三个概念:前置条件,后置条件和不变式。前置条件发生在每个操作(方法,或者函数)的最开始,后置条件发生在每个操作的最后,不变式实际上是前置条件和后置条件的交集。违反这些操作会导致程序抛出异常。
总而言之,DBS是一种对函数的输入和输出所具备的性质是有所期望和规定的编程方法。优点是提高了软件工程的效率和质量,缺点是对程序语言有一定的要求,契约式编程需要一种机制来验证契约的成立与否,并且想要形成这样的编程习惯是一个漫长的过程。
在结对作业中的接口约定应该是一个简单的DSC,负责UI和core的同学商量好接口,调用方确保参数正确,被调用方确保程序的正确性。

2.看教科书和其它资料中关于 Information Hiding,Interface Design,Loose Coupling 的章节,说明你们在结对编程中是如何利用这些方法对接口进行设计的。

Information Hiding(信息隐藏原则):信息隐藏是结构化设计与面向对象设计的基础。代码模块应该采用定义良好的接口来封装,这些模块的内部结构应该是程序员的私有财产,外部是不可见的,外部只用了解他的接口和功能就行了,在结构化中函数的概念和面向对象的封装思想都来源于信息隐藏。

Interface Design(接口设计):接口是面向对象编程语言中接口操作的关键字,功能是把所需成员组合起来,用来装封一定功能的集合。它好比一个模板,在其中定义了对象必须实现的成员,通过类或结构来实现它。调用者无需知道其中的代码。好的接口应该充分说明自己的功能

Loose Coupling(松耦合):松耦合的目标是最小化依赖。藕合度是度量一个代码单元在使用时与其他单元的关系。最理想,最松散的耦合,是一个单元无需其他代码单元特别的配合而可以使用。松耦合的实现能够提高代码的容错率,能让其更容易被其他单元调用。

在我们的结对编程中,一个人负责UI部分,一个人负责core部分,这就需要提前商量好接口的定义和模块的功能,只有在确定下这些之后才能让让我们的工程同步进行。由于我们UI和core分开设计,只要找到接口和我们一样的小组就可以实现程序之间的松耦合。

3.设计的接口概述

public:
	void clear();
	void readdata();
	void readdata_File(const char* name);
	int result();
	void calculate();
	int insertLine(int x1, int y1, int x2, int y2, char type);//0插入,非0出错
	int deleteLine(int x1, int y1, int x2, int y2, char type);//0删除 1出错
	int insertCircle(int x, int y, int r);
	int deleteCircle(int x, int y, int r);
	vector<pair<double, double>> pullIntersect();
	vector<vector<int>> pullgraph();

提供了以上接口,下面一个个说明。

1. clear()接口

其目的在于清空所有数据,当用户想重新计算交点时,使用此函数清空之前存入的几何体数据。

2. readdata()接口

用于从控制台读取数据,每次可以读n个数据。

3. calculate()接口

当用户输入完成后,使用此函数对数据进行计算,并将算出的交点保存。

4. result()接口

返回计算出的交点数目

5. insertLine()接口

这个接口用于满足需求中的“支持插入,删除”。调用此函数会向数据中插入一条直线(会检查是否合法),若返回0表示插入成功,非0表示数据不合法。

6. deleteLine()接口

这个接口为了满足需求中的"支持插入,删除",调用此函数时,若找到用户想要删除的直线,就删除它并且返回0,否则返回1表示未找到。

7. insertCircle()接口

这个接口用于满足需求中的“支持插入,删除”。调用此函数会向数据中插入一个圆(会检查是否合法),返回0表示插入成功,非0表示数据不合法。

8. deleteCircle()接口

这个接口是为了满足需求中的"支持插入,删除",调用此函数时,若找到用户想要删除的圆,就删除它并且返回0,否则返回1表示未找到。

9. pullIntersect()接口

此函数用于提供给前端的UI所有交点数据,使得UI可以根据这些坐标来绘制交点。设计此函数的目的是为了满足需求“支持求解现有几何对象交点并绘制”。

10. pullgraph()接口

此函数用于提供给前端的UI所有几何图形数据,使得UI可以根据这些数据来绘制图像。设计此函数的目的是为了满足需求“支持绘制现有几何对象。”。

11. readdata_File(const char* name)接口

用于从文件中读取数据。

4.模块接口的设计与实现过程

为了实现这些接口,我一共定义了三个类,Line类,circle类和Intersect类,Intersect负责最后的计算等等工作,而Line类和Circle类则是分别对线型和圆形的几何体进行处理,他们之间应该是一种关联的关系。

整个程序的设计大概有3个关键点,第一个是新加入的射线和线段的处理问题,这部分已在本文开头详细介绍;第二个关键是用户可能会有一些错误的数据插入,如何识别出这些问题并且报错,这里我的思路是错误有这么几种,第一种是在输入时的格式有问题,比如我应该输入L 0 0 1 1这样的格式表示一根直线,我少输入了一个数或者开头的L是别的符号等等,这种错误很好处理,在scanf_s时就可判断输入是否合法,第二种是虽然输入格式正确,但是这个参数的范围有问题,超过了极限值或者r小于0,这些错误可以在每次输入后通过if判断是否有问题,第三种则是最麻烦的,就是插入这个几何形后会出现无限多的交点,首先我们考虑圆和圆之间产生无限多交点,那么只有一种可能就是这两个圆是重合的,换句话说必须两个圆的圆心坐标一样且圆半径长一样,那么我们只需要在读入一个圆时与已经存入的所有圆比较,若是有一样的则报错,否则将这个圆加入set,其次是圆和线,必然不会有无限多交点,就不用考虑了,最后是线之间的无限多交点,首先任何线型要出现无限多交点他们必然是重合的,也就是说对于一个方程为

\[Ax+By+C=0 \]

的线l1,和一个方程为

\[ax+by+c=0 \]

的线l2,重合的必要条件是

\[Ab=Ba \]

\[Ac=Ca \]

,其次我们还需要判断这两个线是否真的重合,如果其中一条是直线,那么一定重合,会有无限多个交点,如果没有一条是直线,那么如果两条都是射线,如果两条射线的方向一致,一定重合,如果方向相反,那么一条射线的顶点必须在另一条射线上才会重合,否则不会(这里注意如果只是顶点重合也不会造成无限交点的问题),如果是一条射线一条线段,那么线段的某一端点必须在射线上才会有无限交点问题,如果是两条线段,那么只要l1线段的左端点大于等于另l2线段的右端点,或者l1线段的右端点小于等于l2线段的左端点,就不会有问题,否则会出现无限交点的问题;最后一个关键是精度问题,这里没想出太好的解决方案,只是尽可能的减少了除法,开根号等运算的次数(比如求两直线交点用一般式而不是点斜式),这里也希望大佬提供好的思路。

5. UML图

6.单元测试之接口部分

从难度上看,clear()接口,pullgraph()接口,result()接口,pullintersect()接口都是不需要测试的,因为他们单纯的返回一个数据结构或者是一个简单的初始化过程,而calculate(),readdata()等接口均是从上一次的个人作业中沿用下来的,已进行过测试,因此我们在这里主要测试新增的4个接口

TEST_METHOD(deleteline)
		{
			Intersect p;
			int ret;
			p.insertLine(0, 0, 1, 1, 'L');
			p.insertLine(0, 0, 0, 1, 'R');
			p.insertLine(0, 0, 1, 0, 'S');
			ret = p.deleteLine(1, 1, 2, 2, 'R');
			Assert::AreEqual(ret, (int)1);
			ret = p.deleteLine(1, 1, 2, 2, 'L');
			Assert::AreEqual(ret, (int)0);
			ret = p.deleteLine(0, 0, 1, 1, 'L');
			Assert::AreEqual(ret, (int)1);
			ret = p.deleteLine(0, 0, 0, -1, 'R');
			Assert::AreEqual(ret, (int)1);
			ret = p.deleteLine(0, 0, 1, 2, 'S');
			Assert::AreEqual(ret, (int)1);
		}

这是测试deleteline()接口的单元测试,在这里我设计数据时考虑的主要是即使两线重合了,他们也不是同一条线,不能删除的情况,例如第一个点,两线重合,但一个是直线,一个是射线,故不能删除,返回1表示出错;第二个点是两直线重合的情况,需要删除,返回0;第三个点是一个测试删除成功的点,因为第二个点已经删除了这条直线,第三个点再次请求删除这条直线时应该找不到这条直线了,故返回1;第四个点是测试两射线重合时的情况,两射线重合并且端点相同,但是射线的方向不相同,应当返回1;最后一个测试点时测试两线段的情况,如果两线段一个端点相同另一个不相同,肯定不是同一条线段,应当返回1。

TEST_METHOD(deletecircle)
		{
			Intersect p;
			p.insertCircle(0, 0, 1);
			p.insertCircle(1, 1, 1);
			p.insertCircle(0, 0, 2);
			int ret = p.deleteCircle(1, 1, 1);
			Assert::AreEqual(ret, (int)0);
			ret = p.deleteCircle(1, 1, 1);
			Assert::AreEqual(ret, (int)1);
			ret = p.deleteCircle(0, 0, 1);
			Assert::AreEqual(ret, (int)0);
			ret = p.deleteCircle(0, 2, 1);
			Assert::AreEqual(ret, (int)1);
		}

这是测试deletecircle()接口的单元测试,在这里我主要考虑的特殊点是删除了一个圆后,再次删除这个圆,应当报错,以及两个圆相同的判定必须是圆心相等且半径相同。测试比较简单,不赘述。

TEST_METHOD(insertLine)
		{
			Intersect p;
			int ret = p.insertLine(1, 2, 1, 2, 'L');
			Assert::AreEqual(ret, (int)1);
			ret = p.insertLine(100001, 0, 2, 1, 'L');
			Assert::AreEqual(ret, (int)2);
			ret = p.insertLine(0, -100006, 2, 1, 'L');
			Assert::AreEqual(ret, (int)3);
			p.insertLine(0, 0, 1, 2, 'L');
			ret = p.insertLine(0, 0, -1, -2, 'R');
			Assert::AreEqual(ret, (int)4);
			ret = p.insertLine(0, 0, 1, 5, 'C');
			Assert::AreEqual(ret, (int)5);
			ret = p.insertLine(0, 0, 1, 1, 'L');
			Assert::AreEqual(ret, (int)0);
		}

这是测试insertLine()接口的单元测试。在这里我主要考虑的是插入一个坐标值超过限定的线,插入一个type不符合线标准的值,插入一个两顶点重复的线,插入一个会产生无数交点的线,以及一个可以正常插入的线,这样就考虑到了所有可能异常的情况。

TEST_METHOD(insertCircle)
		{
			Intersect p;
			int ret = p.insertCircle(0, 0, 100005);
			Assert::AreEqual(ret, (int)1);
			ret = p.insertCircle(-100008, 2, 4);
			Assert::AreEqual(ret, (int)2);
			ret = p.insertCircle(0, 2, -2);
			Assert::AreEqual(ret, (int)3);
			p.insertCircle(0, 0, 1);
			ret = p.insertCircle(0, 1, 1);
			Assert::AreEqual(ret, (int)0);
			ret = p.insertCircle(0, 0, 1);
			Assert::AreEqual(ret, (int)4);
		}

这是测试insertcircle()接口的单元测试。在这里我主要考虑的是是插入一个坐标值超过限定的圆,插入一个半径是负数的圆,插入一个会产生无数交点的圆,以及一个可以正常插入的线,这样就考虑到了所有可能异常的情况。

四、支持异常处理

1.支持的异常

异常 描述
Exception_OFB 越界异常,在输入几何体的参数时,如果超出了设定的范围(-100000,100000),或者圆的半径小于0,就会抛出这个异常。
Exception_IP 无限交点异常,当新增一个几何体后,所求交点数目变成无限多个,则会抛出这个异常
Exception_WF 错误格式异常,当输入几何体数据的格式不符合预定格式时,则抛出这个异常
Exception_MD 无效定义异常,当输入的线型几何体的两点坐标相同时,认为这条线是无效的,抛出此异常
异常 目标
Exception_OFB 提醒用户输入的参数范围应该在什么区间内,并且返回到输入处继续输入
Exception_IP 提醒用户加入的这个几何体是禁止的,并返回到输入界面重新输入
Exception_WF 提示用户标准的输入格式,并返回到输入界面继续输入
Exception_MD 提示用户两个点的坐标必须不同,并返回到输入界面继续输入

2.单元测试之异常

我们通过insertLine()函数的测试来测试我们是否正确的抛出异常,因为在每一种异常被抛出后,我们的代码会catch到这个异常,并且return不同的数值,因此利用这一点可以很好地进行单元测试。

TEST_METHOD(insertLine)
		{
			Intersect p;
			int ret = p.insertLine(1, 2, 1, 2, 'L');
			Assert::AreEqual(ret, (int)1);
			ret = p.insertLine(100001, 0, 2, 1, 'L');
			Assert::AreEqual(ret, (int)2);
			ret = p.insertLine(0, -100006, 2, 1, 'L');
			Assert::AreEqual(ret, (int)3);
			p.insertLine(0, 0, 1, 2, 'L');
			ret = p.insertLine(0, 0, -1, -2, 'R');
			Assert::AreEqual(ret, (int)4);
			ret = p.insertLine(0, 0, 1, 5, 'C');
			Assert::AreEqual(ret, (int)5);
			ret = p.insertLine(0, 0, 1, 1, 'L');
			Assert::AreEqual(ret, (int)0);
		}

返回5说明抛出了Exception_WF,返回1说明抛出了Exception_MD,返回2和3说明抛出了Exception_OFB的两种不同格式的异常,返回4说明抛出了Exception_IP,返回0说明这条线是合法的。
通过这个测试,可以发现异常能被正确的抛出且捕获。

3.所有单元测试覆盖率

这是使用OpenCppCoverage测试的覆盖率结果。

五、性能测试以及修改

这是运行2000组数据的性能分析图,可以看到calculate()占用了64.09%的CPU时间,这是由于我们的算法采用暴力的做法,时间复杂度是O(n2),其次是readdata(),这是因为在读入数据时要检查数据的合法性,是O(n)的时间复杂度,所以总共是O(n2)的时间复杂度。

在这组数据中运行次数最多的是calculate_line_line(),这也符合我们之前的推测,由于第一次作业我们发现用set会比map稍微快一点,所以这次也是采用set存点的方法,因此想要改善这里的速度只能更换算法,所以并没有对程序进行太多性能上的优化。

六、添加界面模块

1.界面模块的设计

本着简介明了的设计思路,我设计了两个窗口,一个是图形操作界面,一个是作图界面。

图形操作界面

图形操作界面使用网格布局(gridlayout),界面中有两个列表,两个文本框,五个按钮。左边的列表是存储已有的几何对象,右边的列表是记录当前几何对象的交点。左边的文本框用来输入标准形式的几何图形,add graph按钮将输入的图形加入列表,delete graph按钮将选中的几何图形删除。右边的文本框用来输入需要导入几何对象的txt文件名(全名 xxx.txt),input text按钮确认导入的文件名,draw graph按钮绘制当前列表中的几何图形,draw intersect按钮绘制当前列表中的几何图形并求解交点。

ListWindow::ListWindow(QWidget* parent) :
	QDialog(parent) {
	QGridLayout* gridLayout = new QGridLayout();
	gridLayout->addWidget(textlable1 = new QLabel("Graphs:"), 0, 0, 1, 1);
	gridLayout->addWidget(textlable2 = new QLabel("Input Graph here:"), 0, 1, 1, 1);
	gridLayout->addWidget(textlable3 = new QLabel("Input text name here:"), 0, 2, 1, 1);
	gridLayout->addWidget(textlable4 = new QLabel("Intersects:"), 0, 3, 1, 1);
	gridLayout->addWidget(leftList = new QListWidget(), 1, 0, 6, 1);
	gridLayout->addWidget(rightList = new QListWidget(), 1, 3, 6, 1);
	gridLayout->addWidget(textInput = new QLineEdit(), 1, 1, 1, 1);
	gridLayout->addWidget(add = new QPushButton("add graph"), 2, 1, 1, 1);
	gridLayout->addWidget(deleteButton = new QPushButton("delete graph"), 3, 1, 1, 1);

	gridLayout->addWidget(inputText = new QLineEdit(), 1, 2, 1, 1);
	gridLayout->addWidget(inputButton = new QPushButton("input text"), 2, 2, 1, 1);
	gridLayout->addWidget(drawgraph = new QPushButton("draw graph"), 3, 2, 1, 1);
	gridLayout->addWidget(getIntesect = new QPushButton("draw intersect"), 4, 2, 1, 1);
}

作图界面

构建一个直角坐标系,图形用黑色细线画出,交点用红点标出。

Skepth::Skepth(QWidget *parent)
	: QMainWindow(parent)
{
	ui.setupUi(this);
	image = QImage(1000, 1000, QImage::Format_RGB32);
	QColor backColor = qRgb(255, 255, 255);
	image.fill(backColor);
	Paint();
}

2.两个模块的对接

要求的UI模块需要有4个基础功能

  • 支持从文件导入几何对象的描述。
  • 支持几何对象的添加、删除。
  • 支持绘制现有几何对象。
  • 支持求解现有几何对象交点并绘制。

支持从文件中导入集合对象的描述

由于在core中已经有了从文件中读取数据的接口

void readdata_File(const char* name);

UI只需要获取待读入文件的文件名,再使用接口即可。同时需要在core读取几何图形时更新GraphList。

void ListWindow::getText() {
	QString text = inputText->text();
	p.readdata_File(text.toStdString().c_str());
	leftList->clear();
	vector<vector<int>> temp = p.pullgraph();
	int i, x1, x2, y1, y2, type;
	for (i = 0; i < temp.size(); i++) {
		x1 = temp[i][0];
		y1 = temp[i][1];
		x2 = temp[i][2];
		y2 = temp[i][3];
		type = temp[i][4];
		string graphType, str;
		if (type == 0) {
			graphType = "L ";
		}
		else if (type == 1) {
			graphType = "R ";
		}
		else if (type == 2) {
			graphType = "S ";
		}
		else if (type == 3) {
			graphType = "C ";
		}
		if (type == 0 || type == 1 || type == 2) {
			str = graphType + to_string(x1) + " " + to_string(y1) + " " + to_string(x2) + " " + to_string(y2);
		}
		else if (type == 3) {
			str = graphType + to_string(x1) + " " + to_string(y1) + " " + to_string(x2);
		}
		text = QString::fromStdString(str);
		leftList->addItem(text);
	}
}

支持对集合对象的添加、删除

在core中有相应的添加删除操作

int insertgraph(string s);
int deletegraph(string s);

UI只需要将需要添加或删除的对象通过接口传递给core,同时在core增删集合图形时更改Graph List。

void ListWindow::addGraph() {
	QString text = textInput->text();
	int sigh = p.insertgraph(text.toStdString().c_str());
	if (sigh == 0) {
		leftList->addItem(text);
		textInput->clear();
	}
}

void ListWindow::deleteGraph() {
	QList<QListWidgetItem*> list = leftList->selectedItems();

	if (list.size() == 0) {
		return;
	}
	QListWidgetItem* sel = list[0];
	string item = sel->text().toStdString().c_str();
	int sigh = p.deletegraph(item);
	if (sigh == 0)
	{
		int r = leftList->row(sel);
		leftList->takeItem(r);
	}
}

支持绘制现有几何图像

现有的几何图像存储在core之中,需要通过

vector<vector<int>> pullgraph();

接口来获得一个graph Set。每个graph包含5个参数,type,< x1, y1>, <x2, y2>,type是图形种类的标号直线,射线,线段,圆分别用0,1,2,3来表示,如果图形是圆时,<x2,y2>所表示的是<r,r>。由于图形的比例跨度过大,我们需要先确定能够绘制最大的图形,所以要先遍历所有图形,确定下直角坐标系的比例尺,接下来只需要在直角坐标系中逐一画出图形,最后再标识刻度尺。

void Skepth::getGraph(vector<vector<int>> temp) {
	int i, j;
	int x1, y1, x2, y2, type;
	max = getMax(temp);
	double cap = 500.0 / max;
	for (i = 0; i < temp.size(); i++) {
		x1 = temp[i][0];
		y1 = temp[i][1];
		x2 = temp[i][2];
		y2 = temp[i][3];
		type = temp[i][4];
		if (type == 0 || type == 1 || type == 2) {
			drawAline(x1, y1, x2, y2, type, cap);
		}
		else if (type == 3) {
			drawCylcle(x1, y1, x2, cap);
		}
	}
}

支持求解现有几何对象的交点并绘制

现有几何对象的交点存储在core中,需要通过

vector<pair<double, double>> pullIntersect();

接口来获得一个Point Set。每个Point有两个参数<x1,y1>,只需要遍历Point set并逐一将其画出并添加到右端的Intersect Set。

void ListWindow::calculate() {
	p.calculate();
	Skepth *w = new Skepth;
	w->show();
	w->getGraph(p.pullgraph());
	w->getIntersect(p.pullIntersect()); 
	rightList->clear();
	vector<pair<double, double>> intersectList = p.pullIntersect();
	for (int i = 0; i < intersectList.size(); i++) {
		string point = "<" + to_string(intersectList[i].first) + "," + to_string(intersectList[i].second) + ">";
		rightList->addItem(QString::fromStdString(point));
	}
	w->scalePaint();
}

3.功能演示

添加几何图形

从文件导入图形

删除图形

绘制几何图形

绘制交点

七、结对编程总结

1.结对的过程

我和我的伙伴选择使用腾讯会议作为交流平台,这个平台不但可以共享屏幕,还能实时交流,非常的方便.

2.结对编程的优缺点

优点 描述
合理分工,减轻压力 两个人相比一个人来说最大的好处就是能够分担自己肩上的压力,能够共同克服困难。
相互监督,攻克难关 两个人一起写代码时,保持进度的一致性是至关重要的,一个人落下都不行。
相互检查,减少错误 互相审查代码可以减少bug的出现率。
相互学习,一起进步 在一起编程时培养友谊,一起学习进步。
缺点 描述
想法分歧 两个脑袋就会有两种思想,不同的思想在一起就会出现分歧,所以需要花费一定的时间去协调好两人之间的默契。
工作方式千差万别 如果合作伙伴之间的工作习惯不同,一人白天,一人夜里,就会导致团队缺乏沟通。
效率不同 只用当合作者之间的效率大致相等才能挖掘出合作之间的最高效率,不然只会适得其反。

3.结对每个人的优缺点

  • 伙伴
优点 缺点
工作效率高 主意很多,但大部分难以实现
对我热心帮助
相互之间能够很好的沟通,合作完成项目
  • 自己
优点 缺点
能不拖伙伴的后腿,共同前进 有时会消极怠工,但有伙伴及时提醒
能够静心学习新东西
与伙伴的配合亲密无间
posted @ 2020-03-23 21:36  ybw6  阅读(310)  评论(4编辑  收藏  举报