[Ktor] 实现疫情地图

作者: 何晓杰Dev | 来源:发表于2020-01-30 22:59 被阅读0次

    这一阵子被武汉肺炎搞得完全不敢出门,说好的要去重庆看某人也只能暂时搁置了[手动狗头]。不过身为程序员还是停不住折腾,就画个疫情地图吧。

    当然我们没有一手数据,就先从腾讯这拿了,通过抓包可以知道,疫情地图的数据来自以下 URL:

    $ curl 'https://view.inews.qq.com/g2/getOnsInfo?name=wuwei_ww_area_counts&callback=&_='
    

    在 URL 最后跟上一个 13 位的时间戳就好了。

    那么如何把这些数据变成地图来呈现呢,下面我们就来简单的做个项目吧


    一、建立 Ktor 项目

    如果你使用我以前写的 KtGen 来生成项目,是一点都不麻烦的,完事后可以将 application.conf 的内容改成以下,因为我们这个项目不需要 https 部署,也不需要数据库:

    ktor {
        deployment {
            port = 80
            port = ${?PORT}
        }
        application {
            modules = [ com.rarnu.ncov.ApplicationKt.module ]
        }
    }
    

    接着就可以直接把项目编译通过:

    $ gradle build
    

    二、获取疫情数据

    这个也很简单了,上面已经给出了相关的 URL,我们只需要简单的请求,取回数据后进行包装即可:

    private val dataUrl: String get() = "https://view.inews.qq.com/g2/getOnsInfo?name=wuwei_ww_area_counts&callback=&_=${System.currentTimeMillis()}"
    
    get("/map") {
        call.respondText {
            try {
                JSONArray(JSONObject(HttpClient().get<String>(dataUrl)).optString("data")).filter { country ->
                    (country as JSONObject).optString("country") == "中国"
                }.groupBy { area ->
                    (area as JSONObject).optString("area")
                }.mapValues { confirm ->
                    confirm.value.sumBy { item ->
                        (item as JSONObject).optInt("confirm", 0)
                    }
                }.stringIntToJson()
            } catch (th: Throwable) {
                "[]"
            }
        }
    }
    

    可能有一些同学对这种写法比较陌生,稍做解释:

    // 请求获得疫情数据,解析 JSON 后获取其中的 data 数据,并再次解析为一个 JSONArray
    JSONArray(JSONObject(HttpClient().get<String>(dataUrl)).optString("data")).filter { country ->
        // 过滤出中国的数据,最终得到 List<JSONObject>
        (country as JSONObject).optString("country") == "中国"
    }.groupBy { area ->
        // 按照区域分组,最终得到 Map<String, List<JSONObject>>
        (area as JSONObject).optString("area")
    }.mapValues { confirm ->
        // 对每个分组里的经确诊的感染人数进行求和,最终得到 Map<String, Int>
        confirm.value.sumBy { item ->
            (item as JSONObject).optInt("confirm", 0)
        }
    }.stringIntToJson()
    

    最后一步将 Map<String, Int> 转换为 Json 字符串,用于返回给用户,转换函数如下:

    fun Map<String, Int>.stringIntToJson() = """[${toList().joinToString(",") { """{"name":"${it.first}","value":${it.second}}""" }}]"""
    

    现在把项目跑起来就可以在浏览器里获取到数据了:

    $ gradle run
    

    在浏览器里请求 http://0.0.0.0/map 就可以得到以下数据了:

    [
        {"name":"湖北","value":4523},
        {"name":"广东","value":354},
        {"name":"浙江","value":428},
        {"name":"重庆","value":180},
        {"name":"湖南","value":277},
        {"name":"安徽","value":200},
        {"name":"北京","value":114},
        {"name":"上海","value":112},
        {"name":"河南","value":278},
        {"name":"四川","value":142},
        {"name":"山东","value":158},
        {"name":"广西","value":78},
        {"name":"江西","value":168},
        {"name":"福建","value":101},
        {"name":"江苏","value":129},
        {"name":"海南","value":46},
        {"name":"辽宁","value":41},
        {"name":"陕西","value":63},
        {"name":"云南","value":70},
        {"name":"天津","value":29},
        {"name":"黑龙江","value":43},
        {"name":"河北","value":65},
        {"name":"山西","value":35},
        {"name":"香港","value":10},
        {"name":"贵州","value":11},
        {"name":"吉林","value":9},
        {"name":"甘肃","value":26},
        {"name":"宁夏","value":12},
        {"name":"台湾","value":9},
        {"name":"新疆","value":14},
        {"name":"澳门","value":7},
        {"name":"内蒙古","value":18},
        {"name":"青海","value":6},
        {"name":"西藏","value":1}
    ]
    

    三、数据可视化

    只拿到 Json 还是太原始了,我们得把中国地图画出来,这里我选用 echarts.js 来实现,同时也已经有开源的 china.js 可供使用,所以这项工作就变得非常简单了。

    首先完成一个页面,最简单的就好:

    <html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <script src="../static/js/jquery-2.0.0.min.js"></script>
        <script src='../static/js/echarts.min.js'></script>
        <script src="../static/js/china.js"></script>
    </head>
    <body>
    <div id="map"></div>
    </body>
    </html>
    

    然后写一点 js 就完事了:

    function showMap() {
        $.ajax({
            url: '/map',
            dataType: 'json',
            success: (res) => {
                let optionMap = {
                    backgroundColor: '#FFFFFF',
                    title: {
                        text: '全国疫情数据',
                        x:'center'
                    },
                    tooltip: { trigger: 'item'},
                    visualMap: {
                        show: true,
                        x: 'right',
                        y: 'center',
                        splitList: [{start: 1000},{start: 500, end: 999},{start: 100, end: 499},{start: 10, end: 99},{start: 1, end: 9},{start: 0, end: 0}],
                        color: ['#7D0000','#D52F30','#F4664C','#FFA477','#FFD5C0','#FFF1D5']
                    },
                    series: [{
                        name: '确诊人数',
                        type: 'map',
                        mapType: 'china',
                        roam: false,
                        label: {
                            normal: { show: true},
                            emphasis: { show: false}
                        },
                        data:res
                    }]
                };
                let chart = echarts.init(document.getElementById('map'));
                chart.setOption(optionMap);
            }
        });
    }
    

    好了,现在运行项目就可以看到页面啦:


    四、每日疫情数折线图

    同样的,再写一个接口用于获取数据:

    private val dailyUrl: String get() = "https://view.inews.qq.com/g2/getOnsInfo?name=wuwei_ww_cn_day_counts&callback=&_=${System.currentTimeMillis()}"
    
    get("/daily") {
        call.respondText {
            try {
                val mDate = mutableListOf<String>()
                val mConfirm = mutableListOf<Int>()
                val mSuspect = mutableListOf<Int>()
                val mDead = mutableListOf<Int>()
                val mHeal = mutableListOf<Int>()
                // 请求获得每日情况数据,解析 JSON 后获取其中的 data 数据,并再次解析为一个 JSONArray
                JSONArray(JSONObject(HttpClient().get<String>(dailyUrl)).optString("data")).sortedBy { item ->
                    // 按日期进行排序,最终得到 List<JSONObject>
                    (item as JSONObject).getString("date")
                }.forEach { item ->
                    // 将数据填到列表里
                    with(item as JSONObject) {
                        mDate.add(getString("date").trim())
                        mConfirm.add(getString("confirm").trim().toInt())
                        mSuspect.add(getString("suspect").trim().toInt())
                        mDead.add(getString("dead").trim().toInt())
                        mHeal.add(getString("heal").trim().toInt())
                    }
                }
                // 将数据拼装成 json 返回
                """{"date":${mDate.stringListToJson()},"confirm":${mConfirm.intListToJson()},"suspect":${mSuspect.intListToJson()},"dead":${mDead.intListToJson()},"heal":${mHeal.intListToJson()}}"""
                } catch (th: Throwable) {
                    """{"date":[],"confirm":[],"suspect":[],"dead":[],"heal":[]}"""
                }
            }
        }
    

    同样的,前端依然用 echarts.js 来制作图表:

    function showDaily() {
        $.ajax({
            url: '/daily',
            dataType: 'json',
            success: (res) => {
                let optionMap = {
                    tooltip: {trigger: 'axis' },
                    legend: { data: ['确诊','疑似','死亡','治愈'] },
                    xAxis: [{
                            type: 'category',
                            boundaryGap: false,
                            data: res.date
                        }],
                    yAxis: [{type : 'value'}],
                    series: [
                        {name: '确诊',type: 'line',data: res.confirm,color: '#D52F30'},
                        {name: '疑似',type: 'line',data: res.suspect,color: '#FFA477'},
                        {name: '死亡',type: 'line',data: res.dead,color: '#848586'},
                        {name: '治愈',type: 'line',data: res.heal,color: '#64CC98'}
                    ]
                };
                let chart = echarts.init(document.getElementById('daily'));
                chart.setOption(optionMap);
            }
        });
    }
    

    完成后效果如下所示:


    五、各城市数据列表

    同样的,这个列表也来自于上面的 dataUrl,发起请 求并获取数据即可,不同的地方在于地区下面要有城市列表,并且展示每个城市(区)所对应的数据。我们可以简单的予以处理:

    get("/detail") {
        call.respondText {
            try {
                // 请求获得疫情数据,解析 JSON 后获取其中的 data 数据,并再次解析为一个 JSONArray
                JSONArray(JSONObject(HttpClient().get<String>(dataUrl)).optString("data")).filter { country ->
                    // 过滤出中国的数据,最终得到 List<JSONObject>
                    (country as JSONObject).optString("country") == "中国"
                }.groupBy { area ->
                    // 按照区域分组,最终得到 Map<String, List<JSONObject>>
                    (area as JSONObject).optString("area")
                }.mapKeys { item ->
                    // 更改 map key,将 key 改为一个包含了求和后数据的 Json,最终得到 Map<DataArea, List<JSONObject>>
                    val sumConfirm = item.value.sumBy { i -> (i as JSONObject).optInt("confirm", 0) }
                    val sumDead = item.value.sumBy { i -> (i as JSONObject).optInt("dead", 0) }
                    val sumHeal = item.value.sumBy { i -> (i as JSONObject).optInt("heal", 0) }
                    DataArea(item.key, sumConfirm, sumDead, sumHeal)
                }.mapValues { item ->
                    // 对 area 下属城市,按确诊人数进行逆序排序
                    item.value.sortedByDescending { i -> (i as JSONObject).optInt("confirm") }.map { i ->
                        // 更改 map value,将 value 改为一个包含了 area 对应下属城市的 List,最终得到 Map<DataArea, List<DataCity>>
                        with(i as JSONObject) {
                            DataCity(getString("city"), getInt("confirm"), getInt("dead"), getInt("heal"))
                        }
                    }
                }.dataToJson()
            } catch (th: Throwable) {
                "[]"
            }
        }
    }
    

    其中 dataToJson 扩展的代码如下:

    fun List<DataCity>.cityToJson() = """[${joinToString(",") { """{"city":"${it.city}","confirm":${it.confirm},"dead":${it.dead},"heal":${it.heal}}""" }}]"""
    fun Map<DataArea, List<DataCity>>.dataToJson() = """[${toList().joinToString(",") { """{"area":"${it.first.area}","confirm":${it.first.confirm},"dead":${it.first.dead},"heal":${it.first.heal},"cities":${it.second.cityToJson()}}""" }}]"""
    

    最后,同样用 js 写出一个获取数据并包装出 UI 的函数,此处不再赘述。

    最终实现的效果如下:


    最后我们只需要把页面拼成一个就结束了,当然为了保险起见,避免太多次请求,在真实项目里是需要做缓存的。

    我在这里提供完整项目供大家下载参考,请移步去 Github/rarnu/nCoVMap 啦!

    相关文章

      网友评论

        本文标题:[Ktor] 实现疫情地图

        本文链接:https://www.haomeiwen.com/subject/ltdwthtx.html