这是一个明明可以靠颜值吃饭,而偏偏要选择才华的课设。不,口误,这是一个明明可以靠Excel表格做出,却偏偏选择要用C++实现的课设。开局一张图,内容全靠编(编码的编),开始上图吧~~~
Part 1.数据文件处理
原始数据文件是Earthquake_database1965-2016.txt,由于里面有些数据存在空缺、字串数不定的问题,咱首先用Excel将其导入,稍作修改。比如对分隔月/日/年以及时:分:秒的符号“/”和“:”进行替换,将它们改为一个空格。特别地,由于地震类型(Type)存在Earthquake、Nuclear Explosion和Rock Burst三种类型,字串数为1或2,为了避免读取txt文件造成影响,我们统一将Nuclear Explosion替换为数字0、将Earthquake替换为数字1、将Rock Burst替换为数字2,最终在查询显示信息的时候,咱根据数字代号输出它们的中文名:核爆地震、自然地震和岩爆地震。这样,属于字符型数据的就是最后四组数据:Magnitude Type(震级类型)、ID(地震的编号)、Source(监测地震数据信息的来源)、Status(监测的地震数据状态)。我们将数值型数据和字符型数据分别以txt形式导出,并分别命名为:“Earthquake_database数值数据.txt”和“Earthquake_ database字符数据.txt”。
Part 2.封装与继承设计
对题目要求的各种参数以及设计功能定位,咱将其分为3大类:
1).字符型数据(读取后仅用于输出显示),根据它们所占最大字符数,直接将其定义为字符型数组(全局变量):
#include<iostream>
#include<stdbool.h>
#include<math.h>
using namespace std;
const int N=23412;//总数据量
//字符型数据作全局变量
char MagnitudeType[5];//震级类型
char ID[20];//地震的编号
char Source[15];//监测地震数据信息的来源
char Status[10];//监测的地震数据状态
2).非时间/日期类数值数据,将其封装成Parameter类:
class Parameter{//数值型参数
public:
float Latitude[N];//纬度
float Longitude[N];//经度
float Depth[N];//地震深度
float Magnitude[N];//震级
int Type[N];//地震产生的类型
};
3).时间/日期类数值数据,将其封装成DataTime类,因将其内部设计的函数会访问、调用Parameter类中的数据,因此我们使用公有继承的方式,将其设定为Parameter类的派生类:
class DataTime:public Parameter{//日期与时刻
public:
int Day[N];
int Month[N];
int Year[N];
int Second[N];
int Minute[N];
int Hour[N];
int TargetData[N];//在一定日期范围内的地震行值
int TargetTime[N];//在一定时间范围内的地震行值
int ScreenData();//筛选在日期范围内的地震行值
int ScreenTime();//筛选在时间范围内的地震行值
void Query(bool Choice);//按时间/日期查询功能
void Sort(int Choice);//对应功能3~5的排序函数
void Statistic(bool Choice);//对应功能6~7的统计函数
private:
int Start[3];
int End[3];
};//统计指定位置/时间范围的地震监测信息
然后,对DataTime类中的函数进行类外定义:
(1).ScreenTime()函数:用于筛选选定时间范围的数据。
算法思想:将用户输入的起始和终止时/分/秒分别存入Start和End数组,并对起始与终止时间输入正确性进行判别,一旦出现起始时间比初始时间晚的情况,会输出”输入错误“的提醒并返回整数1;对输入正确的情况,判断已读取数据时间是否在该范围,并依次将符合的数据存入数组TargetTime中,返回值为整数0。
int DataTime::ScreenTime(){//查询指定时间范围的地震监测信息
cout<<"时间输入格式:时/分/秒(用'/'隔开)"<<endl;
cout<<"请输入起始时间:"<<endl;
scanf("%d/%d/%d",&Start[2],&Start[1],&Start[0]);
cout<<"请输入终止时间:"<<endl;
scanf("%d/%d/%d",&End[2],&End[1],&End[0]);
int Min,Max;
Min=Start[2]*3600+Start[1]*60+Start[0];
Max=End[2]*3600+End[1]*60+End[0];
if(Min>Max){cout<<"输入错误"<<endl;return 1;}
else{
for(int i=0,j=0;i<N;i++)
if((Hour[i]*3600+Minute[i]*60+Second[i]>=Min)&&
(Hour[i]*3600+Minute[i]*60+Second[i]<=Max)){
TargetTime[j]=i;j++;
}
return 0;
}
}
(2).ScreenData()函数:用于筛选选定日期范围的数据。
算法思想:类似ScreenTime()函数,将用户输入的起始和终止月/日/年分别存入Start和End数组,并对起始与终止日期输入正确性进行判别,一旦出现起始时间比初始时间晚的情况,会输出”输入错误“的提醒并返回整数1;对输入正确的情况,判断已读取数据时间是否在该范围,并依次将符合的数据存入数组TargetData中,返回值为整数0。
int DataTime::ScreenData(){//查询指定日期范围的地震监测信息
cout<<"日期输入格式:月/日/年(用'/'隔开)"<<endl;
cout<<"请输入起始日期:"<<endl;
scanf("%d/%d/%d",&Start[1],&Start[0],&Start[2]);
cout<<"请输入终止日期:"<<endl;
scanf("%d/%d/%d",&End[1],&End[0],&End[2]);
bool s=true;
if(Start[2]>End[2])s=false;//防错系统
else if(End[2]==Start[2]){
if(Start[1]>End[1])s=false;
else if(Start[1]==End[1]&&Start[0]>End[0])s=false;
}
if(s){//防错系统
int j=0;
for(int i=0;i<N;i++){
int m=0;
if(Year[i]<End[2])m+=1;
else if(Year[i]==End[2]){
if(Month[i]<End[1])m+=1;
else if(Month[i]==End[1]&&Day[i]<=End[0])m+=1;
}
if(m==1){
if(Year[i]>Start[2])m+=1;
else if(Year[i]==Start[2]){
if(Month[i]>Start[1])m+=1;
else if(Month[i]==Start[1]&&Day[i]>=Start[0])m+=1;
}
}
if(m==2){TargetData[j]=i;j++;}
}
for(int i=j;i<N;i++)TargetData[i]=0;
return 0;
}
else{cout<<"输入错误"<<endl;return 1;}
}
值得注意的是,ScreenData()与ScreenTime()对数据是否在用户输入界限范围的判断方式有所不同:一天=24小时=1440分钟=86400秒,ScreenTime()的判断方式就是将时/分/秒转化为秒制,这样谁大谁小就很直观啦;然鹅,年有闰年、平年之分,每个月的天数也无法达成统一标准,那分情况讨论,结果会是:1.数据中的年份是大于、等于、还是小于起始年份?小于的情况直接排除,大于的情况可以直接进入与终止年份的对比判断,等于的情况还要看月份是否大于或等于起始月份?等于起始月份的话还要判断天数是否大于起始日数。然后就算2.数据中的年份是大于、等于、还是小于终止年份……以此类推,这么多种情况,一个个写成if-else语句岂不是要晕了?没关系,我们可以换一种方式去判断,无需将每种情况一个个列出,判断思想如下:A.判断年份是否满足小于等于终止年份的条件,如果小于,初始值为m的变量自增1;否则,如果等于,就判断是否满足小于终止月份或者等于终止月份并且天数小于终止天数的情况,这两种并列情况满足一种m的变量自增1。在此,所有符合条件的情况都会使m=1,不符合条件的情况都无法改变m的原值0。同理,B.年/月/日如果满足起始条件的话,m又会自增1。最终筛选出同时满足A和B条件的情况,即m=2的情况。
PS过渡段:刚才讲解的两个函数都是int型函数,防错系统会将它们的正确与否情况以0和1的形式反馈。接下来讲解的是功能函数,它们会在开始检验ScreenData()/ScreenTime()的值是否为0,如果是1,则不执行其功能,系统会直接返回用户操作界面;等于0则继续执行对应功能。
(3).查询函数Query(bool Choice):题目设定的查询范围有时间与日期之分,Query函数形参Choice是C++语言特有的bool类型,当形参传值为true/非0整数时,整型数组Array会复制TargetData筛选出来的每个数据行值;当形参传值为false/0时,整型数组Array会复制TargetTime筛选出来的每个数据行值。然后依次检索个各行数据的值是否等于Array,由于数组TargetData和TargetTime存储的行值均以从小到大的顺序排列,因此按从小到大的顺序依次检索比对不会遗漏行值。对于符合的数据,不仅将已存入的数值数据输出,还要加上之前未录入的字符数据,打开文件,按数据行值定位并输出字符,接着关闭文件,循环执行此步骤,输出所有符合时间/日期范围的数据。
void DataTime::Query(bool Choice){
int Array[N],s=0;
if(Choice==true){if(ScreenData()==1)s=1;memcpy(Array,TargetData,sizeof(int)*N);}
else{if(ScreenTime()==1)s=1;memcpy(Array,TargetTime,sizeof(int)*N);}
if(!s){
for(int i=0,j=0;i<N;i++){
if(i==Array[j]){
FILE *fp2;
fp2=fopen("Earthquake_database字符数据.txt","r");
for(int k=0;k<=i;k++)fscanf(fp2,"%s%s%s%s\n",&MagnitudeType,&ID,&Source,&Status);
fclose(fp2);
cout<<Month[i]<<"/"<<Day[i]<<"/"<<Year[i]<<" ";
cout<<Hour[i]<<":"<<Minute[i]<<":"<<Second[i]<<" ";
cout<<Latitude[i]<<" "<<Longitude[i]<<" ";
if(!Type[i])cout<<"核爆地震 ";
else if(Type[i]==1)cout<<"自然地震 ";
else cout<<"岩爆地震 ";
cout<<Depth[i]<<"km "<<Magnitude[i]<<"级 ";
cout<<MagnitudeType<<" "<<ID<<" "<<Source<<" "<<Status<<endl;
j++;
}
}
}
}
(4).排序函数Sort(int Choice):依照用户选择的排序对象,形参Choice有3种返回值,分别为:路径A.Choice=0->输入日期和经纬度范围->路径1/路径2:1-选择排序纬度->Choice=1,2-选择排序经度->Choice=2;路径B.Choice=3->输入日期和经纬度范围->排序地震震级;路径C.Choice=4->输入日期和经纬度范围->排序地震深度。然后基于switch语句,将选择对象复制到数组Array中,使用冒泡排序法确定对象的顺序,依次按照从小到大排序而得到的行值存入数组Temp中,并借助for循环依次输出Temp各行值所对应的数据。此函数除了防错功能外,还给用户提供了是否选择经纬度范围的功能,减少了无需选择经纬度范围的用户不必要的操作。
void DataTime::Sort(int Choice){
if(ScreenData()==0){
cout<<"是否选择经纬度范围?是,输入1;否,输入0."<<endl;
float Min[2],Max[2];
bool A;
scanf("%d",&A);
if(A){
cout<<"经/纬度范围输入格式:下限/上限(用'/'隔开)"<<endl;
cout<<"请输入经度范围:(-180~180)"<<endl;
scanf("%f/%f",&Min[0],&Max[0]);
cout<<"请输入纬度范围:(-90~90)"<<endl;
scanf("%f/%f",&Min[1],&Max[1]);
}
else{
Min[0]=Min[1]=0.0;
Max[0]=180.0;Max[1]=90.0;
}
if(Choice<3){
cout<<"请选择排序对象:纬度排序,输入1;经度排序,输入2"<<endl;
scanf("%d",&Choice);
}
int Array[N],Temp[N],n;
FILE *fp3;
fp3=fopen("排序功能.txt","a");
fprintf(fp3,"排序法则:小->大 排序对象:");
switch(Choice){//选择排序对象
case 1:memcpy(Array,Latitude,sizeof(int)*N);fprintf(fp3,"纬度Latitude");break;
case 2:memcpy(Array,Longitude,sizeof(int)*N);fprintf(fp3,"经度Longitude");break;
case 3:memcpy(Array,Magnitude,sizeof(int)*N);fprintf(fp3,"震级Magnitude");break;
case 4:memcpy(Array,Depth,sizeof(int)*N);fprintf(fp3,"深度Depth");break;
}
fprintf(fp3,"\n日期范围:%d/%d/%d~%d/%d/%d 经度范围:%f~%f 纬度范围:%f~%f\n",
Start[1],Start[0],Start[2],End[2],End[1],End[0],Min[0],Max[0],Min[1],Max[1]);
memcpy(Temp,TargetData,sizeof(int)*N);
for(int i=0,j=0;i<N;i++){
if(TargetData[i])j++;
else{n=j-1;break;}
}
for(int i=0;i<n;i++)
for(int j=0;j<n-i;j++)
if(Array[Temp[j]]>Array[Temp[j+1]]){
int t=Temp[j];
Temp[j]=Temp[j+1];
Temp[j+1]=t;
}
for(int i=0,s=0;i<n;i++){
if(Longitude[Temp[i]]<=Max[0]&&Longitude[Temp[i]]>=Min[0]
&&Latitude[Temp[i]]<=Max[1]&&Latitude[Temp[i]]>=Min[1]){
for(int j=0;j<n;j++)if(Temp[i]==TargetData[j])s++;
if(s){s=0;
cout<<Month[Temp[i]]<<"/"<<Day[Temp[i]]<<"/"<<Year[Temp[i]]<<" ";
cout<<Hour[Temp[i]]<<":"<<Minute[Temp[i]]<<":"<<Second[Temp[i]]<<" ";
cout<<Latitude[Temp[i]]<<" "<<Longitude[Temp[i]]<<" ";
fprintf(fp3,"%d/%d/%d %d:%d:%d %f %f ",Month[Temp[i]],Day[Temp[i]],
Year[Temp[i]],Hour[Temp[i]],Minute[Temp[i]],Second[Temp[i]],
Latitude[Temp[i]],Longitude[Temp[i]]);
if(!Type[Temp[i]]){cout<<"核爆地震 ";fprintf(fp3,"核爆地震 ");}
else if(Type[Temp[i]]==1){cout<<"自然地震 ";fprintf(fp3,"自然地震 ");}
else{cout<<"岩爆地震 ";fprintf(fp3,"岩爆地震 "); }
cout<<Depth[Temp[i]]<<"km "<<Magnitude[Temp[i]]<<"级 ";
fprintf(fp3,"%fkm %f级 ",Depth[Temp[i]],Magnitude[Temp[i]]);
FILE *fp2;
fp2=fopen("Earthquake_database字符数据.txt","r");
for(int x=0;x<=Temp[i];x++)fscanf(fp2,"%s%s%s%s",&MagnitudeType,&ID,&Source,&Status);
cout<<MagnitudeType<<" "<<ID<<" "<<Source<<" "<<Status<<endl;
fprintf(fp3,"%s %s %s %s\n",MagnitudeType,ID,Source,Status);
fclose(fp2);
}
}
}
fclose(fp3);
}
}
(5).这是最后一项功能,也是最简单的功能——统计。Statistic(bool Choice)函数中,当形参传值为true/非0整数时,对选定经纬度和时间范围的数据进行筛选;当形参传值为false/0时,对选定时间范围的核爆地震类型数据进行筛选。
void DataTime::Statistic(bool Choice){
if(Choice)ScreenTime();//输入时间范围
int sum=0;
float Min[2],Max[2];//输入经/纬度范围
cout<<"经/纬度范围输入格式:下限/上限(用'/'隔开)"<<endl;
cout<<"请输入经度范围(-180~180):"<<endl;
scanf("%f/%f",&Min[0],&Max[0]);
cout<<"请输入纬度范围(-90~90):"<<endl;
scanf("%f/%f",&Min[1],&Max[1]);
//防错系统
if(Min[1]>Max[1]||Min[0]>Min[0]||fabs(Max[1])>90||fabs(Max[0])>180)cout<<"输入错误"<<endl;
else{for(int i=0,j=0;i<N;i++){
if(Choice==true){
if(i==TargetTime[j]){j++;
if(Longitude[i]<Max[0]&&Longitude[i]>Min[0]
&&Latitude[i]<Max[1]&&Latitude[i]>Min[1])sum++;
}
}
else if(Type[i]==0){
if(Longitude[i]<Max[0]&&Longitude[i]>Min[0]
&&Latitude[i]<Max[1]&&Latitude[i]>Min[1])sum++;
}
}
if(Choice==true)cout<<"在此范围内的地震次数为:"<<sum<<endl;
else cout<<"在此范围内的核爆地震次数为:"<<sum<<endl;
}
}
Part 3.主函数
在主要功能被封装后,主函数的任务就精简得多了:打开并读取、存储装有数值数据的txt文件->基于switch框架对用户选择的功能进行函数调用->用户退出系统->程序结束。
int main(){
DataTime DataTime;//实例化DataTime类
Parameter Parameter;//实例化Parameter类
FILE *fp1;
fp1=fopen("Earthquake_database数值数据.txt","r");
for(int i=0;i<N;i++){
fscanf(fp1,"%d%d%d%d%d%d%f%f%d%f%f",&DataTime.Month[i],&DataTime.Day[i],&DataTime.Year[i],
&DataTime.Hour[i],&DataTime.Minute[i],&DataTime.Second[i],&DataTime.Latitude[i],
&DataTime.Longitude[i],&DataTime.Type[i],&DataTime.Depth[i],
&DataTime.Magnitude[i]);
}
fclose(fp1);
while(1){
cout<<"欢迎使用Tracer地震监测系统,请选择系统功能:"<<endl;
cout<<"1.查询指定时间段地震监测数据,输入1;2.查询指定日期范围地震监测信息,输入2;"<<endl;
cout<<"3.按照地震发生的经/纬度排序,输入3;4.按照地震的震级强度进行排序,输入4;"<<endl;
cout<<"5.按照地震发生的深度进行排序,输入5;6.统计一定范围/时间内地震发生的次数,输入6;"<<endl;
cout<<"7.在一定范围内对因核爆发生的地震进行统计,输入7;退出系统,输入8."<<endl;
cout<<"------------------------------------------------------------------------------"<<endl;
int function,s=0;
scanf("%d",&function);
switch(function){
case 1:DataTime.Query(0);break;
case 2:DataTime.Query(1);break;
case 3:DataTime.Sort(0);break;
case 4:DataTime.Sort(3);break;
case 5:DataTime.Sort(4);break;
case 6:DataTime.Statistic(1);break;
case 7:DataTime.Statistic(0);break;
case 8:s=1;break;
}
cout<<endl;
if(s==1){cout<<"欢迎下次使用Tracer地震监测系统"<<endl;
break;
}
}
return 0;
}
Part 4.系统测试与拓展
(1).操作界面
(2).各项功能
如果输入错误,您还会受到温馨提醒哟~
最后,如果不想继续使用系统,可以直接退出~
生成文件情况:
(3).拓展部分:如果你有世界地图,可以将这些数据点标绘在地图上,从而可以得出哪个位置核爆最多。
将经纬度数据导入Excel表格并做出数据分布座标图,套上半透明的世界地图(最好要标有明确的经纬度参考点):
最后,感谢耐心观看,如有错误,欢迎指正QAQ.