软件工程实践2020_作业4
第二次结对作业:疫情可视化(某次疫情统计可视化的实现)
这个作业属于哪个课程 | 福州大学软件工程2020级春w班 |
---|---|
这个作业要求在哪里 | 结对第二次作业——某次疫情统计可视化的实现 |
结对学号 | 221701127 221701138 |
这个作业的目标 | 某次疫情统计可视化的实现 |
作业正文 | 此文章 |
其他参考文献 | ... |
1.Github仓库地址与代码规范链接
2.成品展示
- 进入网页后显示在最上面的是当前全国某疫情数据统计一览(数据虚构)
- 下滑可看到整张中国地图,左边有颜色对应省份感染人数
- 数据不同时可以看到省份颜色有正确变化,说明数据有正确导入
- 鼠标移到左边导航栏对应区间时全国地图对应省份数值符合的颜色会自动改变高亮
- 单击对应区间可以取消高亮,将对应省份颜色变为白色
- 鼠标移动到省份上会显示省份的当前确诊人数,悬浮窗随着鼠标移动
- 不同省份的数值显然不同,有正确处理
- 单击省份会跳转至新界面,显示该省份的详细疫情统计与折线图,并且选择对应日期最上面的数值会相应变化
- 最上面一排可选择想要看的某一类数据,隐藏其他折线
- 鼠标移动到折线图上对应横坐标有每个纵坐标的值,对应四种不同类型人数
- 当鼠标处于水平线时线会加粗高亮,显示该类人数平均值
3.问题讨论过程
- 最开始有关问题的讨论是数据获取,因为我们两个人都没有python的经验,并且加上在寒假作业时曾经有一个获取txt文本内容的作业,我们就决定将其修改之后拿来获取数据。
之后就是框架的选择,虽然最开始有直接用javaee的想法,但是在讨论和查找资料、询问了一些有开发经验的人之后我们决定使用springboot+thymeleaf,代码的可读性会比较高,而且使用数据会更加方便。之后就是前后端分离,一遍先写静态网页,另一边把后端的控制器等架设好,然后分别测试运行,之后再一起进行交接。 - 在询问过程中还了解到前后端对接方式很多不同,并且由于是第一次做前后端分离,难免会有接口交互不上的情况,于是我们就经常会问一个问题两个人一起讨论半天,学习新框架和技术的过程比较漫长。
一些有关学习框架时候的聊天记录
4.设计实现过程
- 由于是前后端分离开发,前端从静态页面开始构筑,使用echarts先将最终数据要展示的界面做出来,先填充入随机数来看展示效果;后端则是类似JavaEE先将要用的POJO、Controller和DAO等划分功能,然后由第二次寒假作业的函数改写获取TXT内容,分别将多个参数重新定义放在不同类中,最后通过一个控制器统一创建获取TXT内容传参数,先由后端测试能否输出在控制台;
- 等前后端都完成后,将页面的资源src重新写成thymeleaf的要求格式,并且测试是否能够正确引用js文件;后端则通过链接网页测试能否正确将数据输出到网页控制台来检测是否能正确将数据传递到前端,之后再对前后端细节优化。
- 简略项目结构功能图(SpringBoot+Thymeleaf)
5.关键代码
第一部分是网页的代码,通过接收后端数据来填充地图数据,地图由echarts生成,数据动态填充
<script type="text/javascript" th:inline="javascript">
// 全国省份列表
let dataMap = [];
let regions = [[${regions}]];
let a = regions.length;
for (let i = 0; i<a; i++) {
let regionName = String(regions[i].name);
let regionIp = String(regions[i].ip);
let data = {name: regionName, value: regionIp};
console.log(data);
dataMap.push(data);
}
// 绘制图表,准备数据
var option = {
tooltip: {
formatter: function (params) {
let info = '<p style="font-size:18px">'+ params.name + '</p>' +
'<p style="font-size:14px">当前确诊人数:' + params.value + '</p>' +
'<p style="font-size:14px">单击查看此该省(市)详细疫情统计</p>'
return info;
},
backgroundColor: "#fcca42",//提示标签背景颜色
textStyle: { color: "#fff" } //提示标签字体颜色
},
//左侧小导航图标
visualMap: {
show : true,
x: 'left',
y: 'center',
splitList: [
{start: 10000},
{start: 1000, end: 9999},
{start: 100, end: 999},
{start: 10, end: 99},
{start: 1, end: 9},
{start: 0, end: 0},
],
color: ['#660208', '#8C0D0D', '#CC2929','#FF7B69' ,'#FFAA85' , '#F8F9FA']
},
series: [
{
name: '中国',
type: 'map',
mapType: 'china',
selectedMode: 'multiple',
label: {
normal: {
show: true,//显示省份标签
// textStyle:{color:"#c71585"}//省份标签字体颜色
},
emphasis: {
show: true,//对应的鼠标悬浮效果
// textStyle:{color:"#800080"}
}
},
itemStyle: {
normal: {
borderWidth: .5,//区域边框宽度
// borderColor: '#009fe8',//区域边框颜色
// areaColor:"#ffefd5",//区域颜色
},
emphasis: {
borderWidth: .5,
borderColor: '#4b0082',
areaColor: "#ffff80",
}
},
data: dataMap
}
]
};
//初始化echarts实例
var myChart = echarts.init(document.getElementById('container'));
//使用制定的配置项和数据显示图表
myChart.setOption(option);
myChart.on('click',function (params) {
window.location.href="SpecificProvince.html";
})
</script>
控制器和实例化
@Controller
public class IndexController {
@RequestMapping("index")
public String indexController(Model model) throws IOException, ParseException {
RegionDAO regionDAO = new RegionDAO();
List<Region> regions = regionDAO.getRegions();
model.addAttribute("regions", regions);
return "index";
}
@RequestMapping("index/choseDate")
public String choseController(Model model, @RequestParam String date) throws IOException, ParseException {
RegionDAO regionDAO = new RegionDAO();
List<Region> regions = regionDAO.getRegions(date);
model.addAttribute("regions", regions);
return "index";
}
获取文件路径
/**
* 将指定的文件夹类中的文件按名排序
* @param directory 指定文件夹类
*/
public void sortFiles(Directory directory) {
String[] files = directory.getFiles();
if (files.length == 0) return;
List<String> filesList = new ArrayList<>(Arrays.asList(files));
Collections.sort(filesList);
}
/**
* 将将指定日期后的文件名替换为""
* @param files 文件名字符串组
* @param date 指定日期
*/
public void rmAfter(String[] files, String date) {
if (files.length == 0) return;
String[] temps = files.clone();
for (int i = 0; i <temps.length; i++) {
if (temps[i].compareTo(date + ".log.txt") > 0) {
files[i] = "";
}
}
}
/**
* 读取指定文件夹类中指定日期前的日志文件,返回日志列表
* @param directory 指定文件夹类
* @param date 指定日期
* @return 日志列表
* @throws ParseException Log类构造函数进行类型转换
* @throws IOException Log类构造函数中读取日志文件
*/
public List<Log> getLogList(Directory directory, String date) throws ParseException, IOException {
List<Log> logs = new ArrayList<>();
String[] files = directory.getFiles();
if (date != null)
rmAfter(files, date);
for (String file: files) {
if (!file.equals(""))
logs.add(new Log(directory.getPath(), file));
}
return logs;
}
日志文件的处理
/**
* 将日志类列表转换为地区类列表
* @param logs 指定日志类列表
* @return 地区类列表
*/
public List<Region> getRegionList(List<Log> logs) throws IOException {
List<Region> regions = new ArrayList<>();
if (logs == null) return regions;
FileInputStream fileInputStream = null;
InputStreamReader inputStreamReader = null;
BufferedReader bufferedReader = null;
for (Log log : logs) {
fileInputStream = new FileInputStream(log.getPath() + log.getName());
inputStreamReader = new InputStreamReader(fileInputStream, StandardCharsets.UTF_8);
bufferedReader = new BufferedReader(inputStreamReader);
String line;
while ((line = bufferedReader.readLine()) != null)
handleLine(line, regions);
}
if (bufferedReader != null)
bufferedReader.close();
if (inputStreamReader != null)
inputStreamReader.close();
if (fileInputStream != null)
fileInputStream.close();
for (Region region : regions)
region.count();
return regions;
}
/**
* 处理一行日志,转化为地区类并存储在地区列表中
* @param line 日志行
* @param regions 地区列表
* @return 日志行分割结果
*/
public String[] handleLine(String line, List<Region> regions) {
String[] items = null;
RegionDAO regionDAO = new RegionDAO();
if (line.matches("(\\S+) 新增 感染患者 (\\d+)人")) {
items = line.split(" 新增 感染患者 |人");
regionDAO.update(regions, items[0], Type.ip, Integer.parseInt(items[1]));
} else if (line.matches("(\\S+) 新增 疑似患者 (\\d+)人")) {
items = line.split(" 新增 疑似患者 |人");
regionDAO.update(regions, items[0], Type.sp, Integer.parseInt(items[1]));
} else if (line.matches("(\\S+) 感染患者 流入 (\\S+) (\\d+)人")) {
items = line.split(" 感染患者 流入 | |人");
regionDAO.update(regions, items[0], Type.ip, Integer.parseInt(items[2]));
regionDAO.update(regions, items[1], Type.ip, Integer.parseInt("-" + items[1]));
} else if (line.matches("(\\S+) 疑似患者 流入 (\\S+) (\\d+)人")) {
items = line.split(" 疑似患者 流入 | |人");
regionDAO.update(regions, items[0], Type.sp, Integer.parseInt(items[2]));
regionDAO.update(regions, items[0], Type.sp, Integer.parseInt("-" + items[2]));
} else if (line.matches("(\\S+) 死亡 (\\d+)人")) {
items = line.split(" 死亡 |人");
regionDAO.update(regions, items[0], Type.dead, Integer.parseInt(items[1]));
} else if (line.matches("(\\S+) 治愈 (\\d+)人")) {
items = line.split(" 治愈 |人");
regionDAO.update(regions, items[0], Type.cure, Integer.parseInt(items[1]));
} else if (line.matches("(\\S+) 疑似患者 确诊感染 (\\d+)人")) {
items = line.split(" 疑似患者 确诊感染 |人");
regionDAO.update(regions, items[0], Type.ip, Integer.parseInt(items[1]));
regionDAO.update(regions, items[0], Type.sp, Integer.parseInt("-" + items[1]));
} else if (line.matches("(\\S+) 排除 疑似患者 (\\d+)人")) {
items = line.split(" 排除 疑似患者 |人");
regionDAO.update(regions, items[0], Type.sp, Integer.parseInt("-" + items[1]));
}
return items;
}
地区数据的处理
static final String [] regionNames = {
"北京", "天津", "上海", "重庆", "河北", "河南", "云南", "辽宁", "黑龙江",
"湖南", "安徽", "山东", "新疆", "江苏", "浙江", "江西", "湖北", "广西",
"甘肃", "山西", "内蒙古", "陕西", "吉林", "福建", "贵州", "广东", "青海",
"西藏", "四川", "宁夏", "海南", "台湾", "香港", "澳门"
};
/**
* 更新指定地区列表中指定地区指定数值的数据
* @param regions 指定地区列表
* @param name 指定地区的名称
* @param type 指定的类型
* @param num 数值
*/
public void update(List<Region> regions, String name, Type type, int num) {
Region tmp = new Region(name);
if (!regions.contains(new Region(name)))
regions.add(new Region(name));
int index = regions.indexOf(tmp);
Region region = regions.get(index);
if (type.equals(Type.ip))
region.getIpList().add(num);
else if (type.equals(Type.sp))
region.getSpList().add(num);
else if (type.equals(Type.cure))
region.getCureList().add(num);
else if (type.equals(Type.dead))
region.getDeadList().add(num);
}
public void complete(List<Region> regions) {
for (String regionName: regionNames) {
Region region = new Region(regionName);
if (!regions.contains(region))
regions.add(region);
}
}
public List<Region> getRegions() throws IOException, ParseException {
return getRegions(null);
}
public List<Region> getRegions(String date) throws IOException, ParseException {
Directory directory = new Directory("src/main/log/");
DirectoryDAO directoryDAO = new DirectoryDAO();
LogDAO logDAO = new LogDAO();
RegionDAO regionDAO = new RegionDAO();
directoryDAO.sortFiles(directory);
List<Log> logs = directoryDAO.getLogList(directory, date); //获取指定日期前的日志文件列表
List<Region> regions = logDAO.getRegionList(logs); //将日志文件列表转换为地区列表
regionDAO.complete(regions); //补全所有地区,不包括全国统计数据
return regions;
}
6.心路历程与收获
-
LMG:
这次结对第二次作业,深刻让我体会到前后端分离不是一次就能马上配合得好的事,因为这个接口我们浪费了太多时间,万幸最后项目还是及时赶上了。负责前端的我这次在开发静态网页阶段时也学到了有关echarts的使用以及js一些技巧,可谓是受益匪浅,但在这个过程中所遇到的困难实在是让人不得不说:受不了嘞,真的太难了.
话又说回来了,这次一下学到了两个框架,希望能在下一次的开发中派上用场,正所谓学无止境,希望在以后的项目开发中也能不断进步,学到东西。
评价一下结对队友,一句话总结就是:xtg,永远滴神!在这次结对中可以说我们两贡献比是我4他6(其实可能37),很多遇到的比较难的问题都是他先解决然后教我,这可能就是所谓的抱大腿吧。希望在接下来的小组作业中,xtg也能继续发挥超神,克服更多难关。 -
XTG:
这次的作业我们采用了老师没有教过的Spring boot + thymeleaf + echarts进行实现。
由于这两个东西老师没有教过,之前也没有接触过,所以学的很累,也学得不透彻,就算做完了还有很多地方不能理解(主要是thymleaf和echarts部分)
感觉自己模块化的思路不太对劲,但又觉得没啥大问题,估计要有一个标准定下来比较好吧。
只能说搜索引擎的能力也是有限的,真的要debug还得靠自己,只有自己能足够理解才能明白错误出在哪里。(没人教实在是难)
还想吐槽弱类型语言,太怪了。
这次结对结束了,该摸了,还要学Android。