4.计算模块接口的设计与实现过程
我的独到之处在于针对线段与射线的新功能的实现基本都是对原有的直线相关函数的调用,计算交点时先将线段或射线转化为它们所在的直线,直线计算出交点后再判断该点是否在线段或射线上,极大地简化了代码逻辑。
因为考虑到封装的几何对象仅保存数据,不涉及方法,因此用struct封装几何对象
struct Point {//点 double x; double y; double length; bool operator ==(const Point& b) const noexcept { if (compareDouble(x - b.x) == 0 && compareDouble(y - b.y) == 0) return true; return false; } bool operator <(const Point& b) const noexcept { if (compareDouble(x - b.x) == 0 && compareDouble(y - b.y) < 0) return true; if (compareDouble(x - b.x) < 0) return true; return false; } }; typedef Point Vector; //向量 struct Line { //直线 Point p1, p2; }; struct Segment { //线段 Point p1, p2; }; struct Ray { //射线 Point start, direction; }; struct Circle { //圆 Point center; double r; };
通过命令行处理函数,获得输入输出文件的名称。从输入文件中得到所有的几何对象,将同类的几何对象存入同一个vector中。将全体交点的Point
集合存入一个set
。
vector<Line> lineSet; vector<Segment> segmentSet; vector<Ray> raySet; vector<Circle> circleSet; set<Point> pointSet;
计算交点的calPoint
函数在同一个vector内部进行或是在两个不同类vector间进行。计算交点的getPoint
函数在两个几何对象间进行,返回计算出的交点个数:NOCROSS
,ONECROSS
,TWOCROSS
,MANYCROSS
(无数个交点)。
计算交点的函数定义如下
int getPoint(Line l1, Line l2, Point& crossPoint) noexcept; int getPoint(Line l, Segment s, Point& crossPoint) noexcept; int getPoint(Line l, Ray r, Point& crossPoint) noexcept; int getPoint(Line l, Circle c, pair<Point, Point>& crossPair) noexcept; int getPoint(Segment s1, Segment s2, Point& crossPoint) noexcept; int getPoint(Segment s, Ray r, Point& crossPoint) noexcept; int getPoint(Segment s, Circle c, pair<Point, Point>& crossPair) noexcept; int getPoint(Ray r1, Ray r2, Point& crossPoint) noexcept; int getPoint(Ray r, Circle c, pair<Point, Point>& crossPair) noexcept; int getPoint(Circle c1, Circle c2, pair<Point, Point>& crossPair) noexcept; int calPoint(vector<Line>& lineSet, set<Point>& pointSet); int calPoint(vector<Line>& lineSet, vector<Segment>& segmentSet, set<Point>& pointSet); int calPoint(vector<Line>& lineSet, vector<Ray>& raySet, set<Point>& pointSet); int calPoint(vector<Line>& lineSet, vector<Circle>& circleSet, set<Point>& pointSet); int calPoint(vector<Segment>& segmentSet, set<Point>& pointSet); int calPoint(vector<Segment>& segmentSet, vector<Ray>& raySet, set<Point>& pointSet); int calPoint(vector<Segment>& segmentSet, vector<Circle>& circleSet, set<Point>& pointSet); int calPoint(vector<Ray>& raySet, set<Point>& pointSet); int calPoint(vector<Ray>& raySet, vector<Circle>& circleSet, set<Point>& pointSet); int calPoint(vector<Circle>& circleSet, set<Point>& pointSet); int calPoint(vector<Line>& lineSet, vector<Segment>& segmentSet, vector<Ray>& raySet, vector<Circle>& circleSet, set<Point>& pointSet);
将线段或射线转换为直线、判断点是否在一线段或射线范围内的关键函数:
//将线段转化成对应的直线 //将横坐标较小的点作为p1,若两点横坐标相同则将纵坐标较小的点作为p1 Line segmentToLine(Segment s) { Line l; if ((s.p1.x < s.p2.x) || ((s.p1.x == s.p2.x) && (s.p1.y < s.p2.y))) { l.p1 = s.p1; l.p2 = s.p2; } else { l.p1 = s.p2; l.p2 = s.p1; } return l; } //将射线转化成对应的直线 Line rayToLine(Ray r) { Line l{ r.start, r.direction }; return l; } //判断一个点是否在一线段的坐标范围内 int pointIfOnSeg(Point p, Line l) { if (l.p1.x == l.p2.x) { if ((p.y >= l.p1.y) && (p.y <= l.p2.y)) { return ON; } else { return NOTON; } } else { if ((p.x >= l.p1.x) && (p.x <= l.p2.x)) { return ON; } else { return NOTON; } } } //判断一个点是否在一射线的坐标范围内 int pointIfOnRay(Point p, Line l) { if (l.p2.x < l.p1.x) { //若射线指向负方向 if (p.x <= l.p1.x) { return ON; } else { return NOTON; } } else if (l.p2.x == l.p1.x && l.p2.y < l.p1.y) { //若射线指向正下方 if (p.y <= l.p1.y) { return ON; } else { return NOTON; } } else if (l.p2.x == l.p1.x && l.p2.y > l.p1.y) { //若射线指向正上方 if (p.y >= l.p1.y) { return ON; } else { return NOTON; } } else { //若射线指向正方向 if (p.x >= l.p1.x) { return ON; } else { return NOTON; } } }
6.计算模块接口部分的性能改进
使用一个随机生成的包含一千多条数据的测试集。
很明显,程序将大多数时间用在了set的元素插入上。
除此之外,基于确定的精确值对equals意义的重写也占用了大量时间
8.计算模块部分单元测试显示
如上文的模块设计部分展示的函数定义,计算模块分为L-L,L-R,L-S,L-C,S-S,S-R,S-C,R-R,R-C,C-C几个函数,只要分别覆盖了这些这些计算交点的getPoint
函数,就能覆盖其调用的其他计算函数。为了覆盖所有calPoint
函数,再编写一个较大的测试集测试包含所有类别的完整calPoint
函数即可实现较好的覆盖率。
//测试圆与圆交点的多种情况,除保证计算交点个数正确外,还要保证计算的交点坐标正确 TEST_METHOD(Circle_Circle_TwoCross) { Circle c1; Circle c2; c1.center.x = 0; c1.center.y = 0; c1.r = 2; c2.center.x = 2; c2.center.y = 0; c2.r = 2; Point realPoint1, realPoint2; pair<Point, Point> testPair; realPoint1.x = 1; realPoint1.y = 1.73205081; realPoint2.x = 1; realPoint2.y = -1.73205081; Assert::IsTrue(getPoint(c1, c2, testPair) == 2); Assert::IsTrue(((realPoint1 == testPair.first) && (realPoint2 == testPair.second)) || ((realPoint2 == testPair.first) && (realPoint1 == testPair.second))); } TEST_METHOD(Circle_Circle_OneCross) { Circle c1; Circle c2; c1.center.x = 0; c1.center.y = 0; c1.r = 2; c2.center.x = 4; c2.center.y = 0; c2.r = 2; Point realPoint1, realPoint2; pair<Point, Point> testPair; realPoint1.x = 2; realPoint1.y = 0; Assert::IsTrue(getPoint(c1, c2, testPair) == 1); Assert::IsTrue(realPoint1 == testPair.first); } TEST_METHOD(Circle_Circle_NoCross) { Circle c1; Circle c2; c1.center.x = 0; c1.center.y = 0; c1.r = 2; c2.center.x = 5; c2.center.y = 0; c2.r = 2; Point realPoint1, realPoint2; pair<Point, Point> testPair; Assert::IsTrue(getPoint(c1, c2, testPair) == 0); } TEST_METHOD(TestAll) { vector<Line> lineSet; vector<Segment> segmentSet; vector<Ray> raySet; vector<Circle> circleSet; set<Point> pointSet; Line l1, l2; Segment s1, s2, s3; Ray r1, r2, r3; Circle c1, c2; l1.p1.x = 1; l1.p1.y = 2; l1.p2.x = 3; l1.p2.y = 3; l2.p1.x = 4; l2.p1.y = 2; l2.p2.x = 2; l2.p2.y = -4; lineSet.push_back(l1); lineSet.push_back(l2); s1.p1.x = -5; s1.p1.y = 1; s1.p2.x = 5; s1.p2.y = -3; s2.p1.x = 5; s2.p1.y = 3; s2.p2.x = 2; s2.p2.y = 2; s3.p1.x = 6; s3.p1.y = -5; s3.p2.x = 6; s3.p2.y = 6; segmentSet.push_back(s1); segmentSet.push_back(s2); segmentSet.push_back(s3); r1.start.x = 5; r1.start.y = 2; r1.direction.x = 3; r1.direction.y = 4; r2.start.x = 2; r2.start.y = -4; r2.direction.x = -1; r2.direction.y = -1; r3.start.x = -10; r3.start.y = 1; r3.direction.x = 10; r3.direction.y = 1; raySet.push_back(r1); raySet.push_back(r2); raySet.push_back(r3); c1.center.x = 2; c1.center.y = 3; c1.r = 4; c2.center.x = 5; c2.center.y = 2; c2.r = 2; circleSet.push_back(c1); circleSet.push_back(c2); Assert::AreEqual(calPoint(lineSet, segmentSet, raySet, circleSet, pointSet), 31); }
覆盖率截图,计算模块GeoCalculate.pp
的覆盖率达到了90%:
9.计算模块部分异常处理说明。在博客中详细介绍每种异常的设计目标。每种异常都要选择一个单元测试样例发布在博客中,并指明错误对应的场景。
设计了以下几类异常:
- 非法输入格式
- 输入几何对象不足
- 文件尾部输入过多内容
- 坐标范围超出限制
- 命令行参数是其他字符
- 交点个数为无数个
非法输入格式
当遇到不符合要求的输入数据时,如文件开始未出现n、出现非法字符、几何对象的数据不正确时,抛出异常 illegalInputPattern
单元测试样例:
输入几何对象不足
从文件的首行获得n,但是当在文件之后输入数据不足n个时,抛出异常notEnoughInputElement
单元测试样例:
文件尾部输入过多内容
当读入n和n个几何对象的数据后,文件尾还有多余内容,抛出异常TooManyInputElements
单元测试样例:
坐标范围超出限制
对于每个输入数据,判断是否在(-100000,100000)范围内,如果不在,则抛出异常outRangeException
。当圆的输入半径小与或等于0时,也会抛出这一异常。
单元测试样例:
命令行参数是其他字符
若命令行参数不正确,则抛出异常commandException
。
单元测试样例:(命令行参数为"-d")
交点个数为无穷个
若两个图形有无数个交点,则抛出异常infException
。
单元测试样例:(两条直线重合)
10.界面模块的详细设计过程。在博客中详细介绍界面模块是如何设计的,并写一些必要的代码说明解释实现过程。
界面模块使用Qt Creator进行开发,设计了两个窗口,一个是主界面dialog,另一个是画图界面new_window。
主界面如图所示:
接下来逐一介绍所实现的四个功能:
-
打开文件
void Dialog::readFile()
函数使用了
getOpenFileName
方法获取文件路径,并打开文件,用readLine
方法逐行读取文件内容,并用split
方法对字符串按空格进行分割,存储到相应的结构中。最后使用ui->label->setText
方法将读取的内容显示在界面上。点击“打开文件”按钮并选择文件后效果如图所示:
-
添加图形
void Dialog::addone()
函数使用
ui->text->toPlainText
方法获取文本框中的字符串,用split
方法分割并存储到相应的结构中。最后使用ui->label_2->setText
方法将所添加的数据显示在界面上。点击“添加”按钮后效果如图所示:
-
删除图形
void Dialog::deleteone()
函数使用
ui->text->toPlainText
方法获取文本框中的字符串,用split
方法分割,找到该数据对应的元素并删除。最后使用ui->label_2->setText
方法将所删除的数据显示在界面上。点击“删除”按钮后效果如图所示:
-
绘制图形和交点
void Dialog::open()
函数打开新窗口new_window,重写
paintEvent
方法,进行图像绘制。绘图方法:
- 线:drawLine
- 圆:drawEllipse
- 点:drawPoint
图形绘制效果如图所示:
11.界面模块与计算模块的对接。详细地描述 UI 模块的设计与两个模块的对接,并在博客中截图实现的功能。
计算交点需要用到calPoint
方法,它的接口是五个结构体的集合,分别代表直线、线段、射线、圆和点。我的想法是把界面模块中的结构体也包装成相同的形式,直接调用该方法进行计算即可。将Release版的动态链接库和相关头文件添加到项目中,即可直接调用该方法。
功能实现展示
从文件中添加3个图形,并手动添加1个图形(射线),如图所示:
点击“绘制图形和交点”按钮后效果如图所示:
从文件中添加4个图形,并手动删除1个图形(线段),如图所示:
点击“绘制图形和交点”按钮后效果如图所示:
来源:https://www.cnblogs.com/syncline/p/12559007.html