zoukankan      html  css  js  c++  java
  • 结对第二次作业——某次疫情统计可视化的实现

    这个作业属于哪个课程 https://edu.cnblogs.com/campus/fzu/2020SpringW/
    这个作业要求在哪里 https://edu.cnblogs.com/campus/fzu/2020SpringW/homework/10456
    结对学号 221701102 221701339
    这个作业的目标 疫情统计可视化的实现
    作业正文 https://www.cnblogs.com/Zhifeng-Shen/p/12465684.html
    其他参考文献 ...

    一、git仓库链接 代码规范链接

    二、成品展示

    视频演示
    (可能有广告,请耐心等待~)

    新型冠状病毒疫情数据服务平台采用地图与数据结合的方式,展示疫情情况。页面上方以数字形式显示疫情数据,页面下方以地图展示疫情的全国分布情况,具体省份以折线图展示疫情的增减趋势。

    全国疫情数据:以不同文字颜色显示数据截止日期前的现有确诊人数、现有疑似人数、现有治愈人数、现有死亡人数,并统计与昨日对比的的增减趋势。

    用户可以利用右上角的选择日期按钮,指定数据的截止日期,我们提供日期选择框方便用户选择日期。

    中国新型冠状病毒疫情图:以不同颜色代表不同的确诊人数区间,颜色随着确诊人数的增加变深,颜色越深代表此地区疫情情况越严重。右侧提供数据视图按钮与保存图片按钮。点击保存图片按钮,将当前查看的地图保存为"中国新型冠状病毒疫情图.png"。

    鼠标移动到具体省份上可以高亮显示,点击具体省份,显示省份名称、确诊人数与查看详情按钮。

    鼠标移动到左下角的取色器可以高亮显示指定区间的省份,点击可展示或取消展示。

    可查看当前地图显示疫情情况对应的数据视图。

    具体省份疫情情况:以不同文字颜色显示数据截止日期前的现有确诊人数、现有疑似人数、现有治愈人数、现有死亡人数,并统计与昨日对比的的增减趋势。同样可以选择指定的截止日期。

    以折线图形式显示新增确诊趋势、新增疑似趋势、死亡/治愈趋势,鼠标移动到折点上可以显示具体日期与具体疫情情况。

    三、结对过程

    即刚开始拿到题目后,和队友怎么讨论,解决问题和查找资料的过程,并提供两人结对讨论的截图

    由于两个人都是第一次接触到Github团队协作,首先通过讨论和实践,熟悉了多人合作相关功能如dev分支、冲突的处理与合并等。

    • 查找资料与分工的过程,由221701339负责后端与前后端接口部分,221701102负责前端。

    • 后端先完成,提供API给前端调用。

    • 前端基本完成,展示效果

    • 对于界面美化的讨论,并协助修改了BUG|ू・ω・` )

    • 依然是协助修改BUG,最后完美收工

    • 关于为什么没有部署到服务器:

    四、设计实现过程

    ​ 总体思路:后端提供API接口,把经过一定处理的数据返回给前端,前端请求API取得数据并展示。

    ​ 后端:Spring Boot,前端:Angular

    1.后端

    ​ 为了检验之前学习路线中Spring Boot成果,特地采用了它。选择Spring Boot很主要的原因是,Spring Boot 提供约定优于配置方式,免去大量配置工作,同时构建Web API变得非常容易,通过Java提供注解特性也大大简化了配置工作。还提供内嵌Tomcat容器,方便部署。

    a.数据来源(Data )

    ​ 这里使用之前作业提供日志文件作为数据源,一方面是为了服用之前经过验证的可靠代码,另一方面是如果使用爬虫或者调用第三方API需要一定的学习成本,且可能不稳定。同时感谢徐助教提供的日志数据。

    d.数据访问(DAO)

    ​ 这里我们复用了之前读取处理的日志文件代码。但是,以前的代码提供返回数据并不是我们想要的。因此我们增加获取国家和省份两种数据方式。

    b.业务逻辑(Service)

    ​ 有了前面的铺垫后,我们使用业务逻辑只需把处理好的数据包装成一定格式即可。

    c.控制器(Controller)

    ​ 控制器只负责接受请求,把参数进行简单处理,并传递给Servicec层处理,返回最终结果结果。

    d.配置

    ​ 为了前端访问,我们需要配置跨域。

    ​ 由于采取读取文件日志形式,因此对于参数相同请求的请求,我们通过缓存已经处理结果来减少不必要的I/O时间,下次再有相同参数的请求同时直接使用缓存的结果,当然这非常适合不会修改的日志文件这种情况。

    e.提高灵活性

    ​ 在Spring Boot中有个application.properties配置文件,可以配置一些参数,也可以自定配置参数。同时也可以通过启动的命令参数、环境变量来覆盖application.properties提供的默认值。

    ​ 因为是使用读取日志文件来获取数据,因此需要提供的日志文件的目录和文件的编码方式。

    ​ 因此我们定义了infectstatistic.log.pathinfectstatistic.log.encoding配置项指定日志文件的目录和文件的编码方式。这些配置项可以通过启动项目附带命令参数指定或者直接修改application.properties等来进行灵活配置。同时我们指定了默认值。有关Spring Boot外部配置参见:Spring Boot Externalized Configuration

    d.接口

    ​ 有两个接口,一个是用于返回国家总体疫情情况、历史疫情数据和和其他省份总体疫情数据。

    GET http://localhost:8080/statistics/v1/overview
    url参数:
    	endDate:字符串,值格式为yyyy-mm-dd,必须指定。指定数据直截止日期。
    返回示例:
    {
        "self": {
            "name": "中国",
            "total": {
                "patient": 178,
                "survivor": 27,
                "suspect": 317,
                "dead": 21
            },
            "history": [
                {
                    "key": "2020-01-20",
                    "value": {
                        "patient": 31,
                        "survivor": 0,
                        "suspect": 0,
                        "dead": 0
                    }
                },
                {
                    "key": "2020-01-21",
                    "value": {
                        "patient": 0,
                        "survivor": 0,
                        "suspect": 0,
                        "dead": 0
                    }
                }
            ]
        },
        "children": [
            {
                "key": "黑龙江",
                "value": {
                    "patient": 1,
                    "survivor": 0,
                    "suspect": 0,
                    "dead": 0
                }
            },
            {
                "key": "西藏",
                "value": {
                    "patient": 1,
                    "survivor": 0,
                    "suspect": 0,
                    "dead": 0
                }
            }
        ],
        "_links": {
            "self": {
                "href": "http://localhost:8080/statistics/v1/overview?endDate=2020-03-10"
            }
        }
    }
    
    

    ​ 另一个接口返回省份总体疫情情况和历史疫情数据。

    GET http://localhost:8080/statistics/v1/detail
    url参数:
    	name:字符串,省份名称,必须指定。
    	endDate:字符串,值格式为yyyy-mm-dd,必须指定。指定数据直截止日期。
    返回示例:
    {
        "name": "福建",
        "total": {
            "patient": 0,
            "survivor": 0,
            "suspect": 0,
            "dead": 0
        },
        "history": [
            {
                "key": "2020-01-20",
                "value": {
                    "patient": 0,
                    "survivor": 0,
                    "suspect": 0,
                    "dead": 0
                }
            },
            {
                "key": "2020-01-21",
                "value": {
                    "patient": 0,
                    "survivor": 0,
                    "suspect": 0,
                    "dead": 0
                }
            }
        ],
        "_links": {
            "self": {
                "href": "http://localhost:8080/statistics/v1/detail?name=%E7%A6%8F%E5%BB%BA&endDate=2020-03-10"
            }
        }
    }
    

    2.前端

    ​ 这里我们使用Angular作为前端框架,官方推荐是TypeScript作为开发语言,TypeScript提供面向对象语法方式、同时类型安全也得到了增强,可以减少出错可能。Angular还提供依赖注入框架等,来提升开发效率和模块化程度。

    ​ 前端主要分为两个视图,一个是全国视图,另一个是省份视图。

    a.数据展示

    ​ 数据展示主要分为文本和图标。文字展示显示数据总体情况,二图标展示的是数据的分布以及变化趋势。

    ​ 这里图表控件来自ECharts,使用的是图表有地图、和折线图。为了让数据方便绑定,还是利用了ngx-echarts拓展来实现。

    b.界面布局

    ​ 为了使得构建响应式网页,我们使用了Bootstrap提供的代码设计页面布局,同时Bootstrap还提供了丰富而美观的控件。为了方便动态内容切换和减少外部JavaScript干扰,我们使用了ng-bootstrap拓展来实现。

    c.数据获取

    ​ 由于后端已经规定好了接口,前端就可以直接调用了。因此我们可以定义一个数据服务来获取后端数据,并把它注入Angular依赖注入容器中。

    ​ 不过后端返回并不是都可以直接使用,需要转换成一定格式的数据才能直接显示或者提供给图表控件。

    五、功能结构图

    六、代码说明

    1.后端

    ​ 为了适应新的数据要求,我们定义以下的pojo:

    ​ InfectionCell类表示基本疫情数据,包括:确诊人数、治愈人数、疑似人数、死亡人数

    public class InfectionCell {
        /**
         * 确诊人数
         */
        private int patient;
        /**
         * 治愈人数
         */
        private int survivor;
        /**
         * 疑似人数
         */
        private int suspect;
        /**
         * 死亡人数
         */
        private int dead;
    }
    
    

    DetailInfectionItem类表示省份疫情数据,包括:省份名称、总体疫情数据,省份历史疫情数据。

    public class DetailInfectionItem {
        /**
         * 省份名称
         */
        private String name;
        /**
         * 省份基本疫情数据
         */
        private InfectionCell total;
        /**
         * 省份历史疫情数据
         */
        private List<Pair<LocalDate, InfectionCell>> history;
    }
    

    OverviewInfectionItem类表示国家疫情数据,包括:国家总体疫情数据、各省份总体数据。

    public class OverviewInfectionItem {
        /**
         * 国家基本疫情数据
         */
        private DetailInfectionItem self;
        /**
         * 国家各省份基本疫情数据
         */
        private Collection<Pair<String, InfectionCell>> children;
    }
    

    ​ 获取数据,包括获取省份数据和全国数据。这里复用之前的作业(点击链接查看)写的代码,不过只是有关于数据读取处理的代码,这里只解释新的代码,旧的代码解释可以在之前的作业找到。因为有新的数据需求,原来处理数据的InfectStatistician 类需要增加两个方法getCountryStatistics()getProvinceStatistics

    /**
     * 从处理好的日志数据,统计国家疫情数据
     *
     * @param name 国家名
     * @return 国家疫情数据
     */
    public OverviewInfectionItem getCountryStatistics(String name) {
        if (!ready) {
            throw new InfectStatisticException("无法执行操作,请重新取数据");
        }
    
        OverviewInfectionItem overview = new OverviewInfectionItem();
        Map<String, InfectionCell> map = new HashMap<>(257);
        DetailInfectionItem all = new DetailInfectionItem();
        all.setName(name);
        all.setHistory(new LinkedList<>());
        all.setTotal(new InfectionCell());
        overview.setSelf(all);
        overview.setChildren(new LinkedList<>());
    
        data.sort((o1, o2) -> o1.getKey().compareTo(o2.getKey()));
        Iterator<Pair<LocalDate, Collection<InfectionItem>>> iterator = data.listIterator();
        Pair<LocalDate, Collection<InfectionItem>> pair = null;
        if (iterator.hasNext()) {
            pair = iterator.next();
        }
        LocalDate current = minDate.plusDays(0);
        while (endDate.isAfter(current) || endDate.isEqual(current)) {
            LocalDate date = current;
            InfectionCell day = new InfectionCell();
            if (pair != null && pair.getKey().equals(date)) {
                for (InfectionItem item : pair.getValue()) {
                    updateInfectionCellBy(item, day);
                    updateInfectionCellBy(item, all.getTotal());
    
                    InfectionCell province = getOrCreateFrom(map, item.name);
                    updateInfectionCellBy(item, province);
                }
                if (iterator.hasNext()) {
                    pair = iterator.next();
                } else {
                    pair = null;
                }
            }
            all.getHistory().add(new Pair<>(current, day));
            current = current.plusDays(1);
        }
    
        List<Pair<String, InfectionCell>> children = new LinkedList<>();
        for (String key : map.keySet()) {
            children.add(new Pair<>(key, map.get(key)));
        }
        overview.setChildren(children);
        return overview;
    }
    
    

    getCountryStatistics()方法首先初始化要返回的OverviewInfectionItem,接着按日期升序排序每个日志文件处理的结果列表data

    ​ 之后遍历结果列表data,然后按日志文件的日期到传入参数endDate开始循环,如果当前日期与结果列表data有日期匹配,遍历结果列表data日期匹配匹配的项目,更新省份、全国的数据。之后生成一个当日全国疫情数据加入到OverviewInfectionItemhistory中,如果之前结果列表data没有日期匹配匹配的项目,则当天数据默默认都为0。

    ​ 遍历结束后,把日志文件出现的省份所对应的疫情数据InfectionItem添加到OverviewInfectionItemchildren中。

    /**
     * 从处理好的日志数据,统计指定省份疫情数据
     *
     * @param province 省份名称
     * @return 省份疫情数据
     */
    public DetailInfectionItem getProvinceStatistics(String province) {
        if (!ready) {
            throw new InfectStatisticException("无法执行操作,请重新取数据");
        }
    
        DetailInfectionItem all = new DetailInfectionItem();
        all.setName(province);
        all.setTotal(new InfectionCell());
        all.setHistory(new LinkedList<>());
    
        data.sort((o1, o2) -> o1.getKey().compareTo(o2.getKey()));
        Iterator<Pair<LocalDate, Collection<InfectionItem>>> iterator = data.listIterator();
        Pair<LocalDate, Collection<InfectionItem>> pair = null;
        if (iterator.hasNext()) {
            pair = iterator.next();
        }
        LocalDate current = minDate.plusDays(0);
        while (endDate.isAfter(current) || endDate.isEqual(current)) {
            LocalDate date = current;
            InfectionCell day = new InfectionCell();
            if (pair != null && pair.getKey().equals(date)) {
                for (InfectionItem item : pair.getValue()) {
                    if (item.name.equals(province)) {
                        updateInfectionCellBy(item, day);
                        updateInfectionCellBy(item, all.getTotal());
                    }
                }
                if (iterator.hasNext()) {
                    pair = iterator.next();
                } else {
                    pair = null;
                }
            }
            all.getHistory().add(new Pair<>(current, day));
            current = current.plusDays(1);
        }
        return all;
    }
    

    getProvinceStatistics()方法与getCountryStatistics()流程类似,不过在遍历过程中只添加更新参数province指定省份的总体疫情数据和历史疫情数据。

    2.前端

    ​ 前端使用StatisticsService服务来从后端获取数据,该服务被注入到根模块中。

    export class StatisticsService {
      private url: string = "http://localhost:8080/statistics/v1/";
    
      constructor(private http: HttpClient) {
      }
    
      getDetailStatistics(name: string, endDate: Date): Observable<DetailItem> {
        return this.http.get<DetailItem>(this.url + "detail"
          , {
            params: {
              name: name,
              endDate: formatDate(endDate, "yyyy-MM-dd", "zh-Hans")
            }
          })
          .pipe(
            catchError(this.handleError<DetailItem>('getDetailStatistics', null))
          );
      }
    
      getOverviewStatistics(name:string,endDate:Date):Observable<OverviewItem>{
        return this.http.get<OverviewItem>(this.url + "overview",
          {
            params: {
              name: name,
              endDate: formatDate(endDate, "yyyy-MM-dd", "zh-Hans")
            }
          })
          .pipe(
            catchError(this.handleError<OverviewItem>('getOverviewStatistics', null))
          );
      }
    
      private handleError<T> (operation = 'operation', result?: T) {
        return (error: any): Observable<T> => {
          console.error(error);
          return of(result as T);
        }
      }
    }
    

    ​ 为了把后端的数据转化成图表控件可接受的数据格式,定义了一个工具类:

    export class StatisticsUtil {
      static getKeyValues<K, V>(pairs: KeyValuePair<K, V>[]) {
        let values: K[] = [];
        if (pairs) {
          for (let pair of pairs)
            values.push(pair.key);
    	}
    	return values;
      }
    
        static getPatientValues<K>(pairs:KeyValuePair<K,InfectionCell>[]):number[]{
            let values:number[]=[];
            if(pairs){
                for (let pair of pairs){
                    let cell:InfectionCell=pair.value;
                    values.push(cell.patient);
                }
            }
            return values;
        }
    
        static getSurvivorValues<K>(pairs:KeyValuePair<K,InfectionCell>[]):number[]{
            let values:number[]=[];
            if(pairs){
                for (let pair of pairs){
                    let cell:InfectionCell=pair.value;
                    values.push(cell.survivor);
                }
            }
            return values;
        }
    
        static getSuspectValues<K>(pairs:KeyValuePair<K,InfectionCell>[]):number[]{
            let values:number[]=[];
            if(pairs){
                for (let pair of pairs){
                    let cell:InfectionCell=pair.value;
                    values.push(cell.suspect);
                }
            }
            return values;
        }
    
      static getDeadValues<K>(pairs: KeyValuePair<K, InfectionCell>[]): number[] {
        let values: number[] = [];
        if (pairs) {
          for (let pair of pairs) {
            let cell: InfectionCell = pair.value;
            values.push(cell.dead);
          }
        }
        return values;
      }
    
    }
    

    getKeyValues()方法用于获取由{ key:K; value:V; }对象构成的数组的key属性数组,getPatientValues()方法用户获取由{ key:K; value:V; }对象构成的数组中value对象的patient属性数组,其他getDeadValues()getSurvivorValues()getSuspectValues()getPatientValues()类似,只不过获取value对象的对应属性数组。

    七、心路历程与收获

    221701339

    首先阅读《构建之法》以来,收获最多的是软件开发流程要关注的地方,比如代码规范、单元测试、代码复审。这些是对于初入软件工程容易犯错或忽视的地方,我们一直在试错,需要不断总结自己来改进以后的行为。《构建之法》提供了大量的方法论,当自己做了相关工作之后,回过来再仔细品味,总有一番收获。

    对于团队合作或者结对编程,如果大家都是心有灵犀一点通,那么工作起来就能得心应手。然而这只是理想情况,不同人还是存在差异的,比如技术、性格、目标,这些差异化会导致团队或者结对,出现不同步。当然需要有人来及时矫正。

    这次结对过程,其实充满了挑战。在VS Code的Git操作上捣鼓了很久,我之前一直使用的是IDEA自带Git插件;对于未知的领域学习需要一定时间,但是面对这种紧急的任务,需要不断讨论和帮助;需要合理的时间安排,因为大家选课不是都一样的,因此需要安排时间进行项目讨论,同时话还面临其他科目在时间上的管理。等等。

    我觉得如果时一周时间只完成这个任务,而没有其他事件的干扰,那么收获得可能更多。

    221701102

    在《构建之法中》读到“结对编程使程序的设计和代码质量都有了进一步的提升”,简直不能再同意。回想起自己完成的那次寒假作业,我需求分析都做了好几天,但这次结对作业,效率很高地就完成了。通过这次结对,我对团队协作有了更深的了解,一个人完成代码开发工作量有一点点大,对需求的理解和实现都会因为主观因素带来偏差,但团队协作不一样,多个人的灵感能够碰撞出不一样的火花,带来质量的提升。

    同时我的队友熟悉且擅长前端框架,所以在框架的搭建、BUG修改方面帮到了我很多,肥肠感谢他。

    八、评价队友

    我的队友是一名效率高、认真负责、条理清晰、编码能力强的优秀软工学子。————221701102

    我的队友很好沟通,同时也善于学习,做事也比较认真。————221701339

  • 相关阅读:
    嵌入式Linux的启动过程
    【转载】vim 中文帮助手册的安装
    面向对象之编写驱动程序--中断(linux系统、s3c6410开发板)
    【转】DBCC IND / DBCC PAGE
    【转】索引查询 sys.dm_db_index_physical_stats
    【tag】Enum.HasFlag 方法
    【tag】Tuple 类 使用介绍
    【fixed point】柯里化(currying) C#实现
    SqlDataAdapter 批量更新 DataTable
    sqlCacheDependency 使用
  • 原文地址:https://www.cnblogs.com/Zhifeng-Shen/p/12465684.html
Copyright © 2011-2022 走看看