高质量编码-空气质量地图可视化

2022-01-07 15:24:41 浏览数 (1)

平时采用百度地图api来开发地图可视化,除了使用JavaScript API GL来开发地图可视化应用。也可以使用百度地图提供的MapVGL库来开发。使用JavaScript API GL时利用overlayer(label,marker,line,polygon等)来完成地图叠加层添加实现可视化,MapVGL由于采用了图层(layer)来群组管理叠加层,从而代码更简洁,开发更高效。

百度地图JavaScript API GL v1.0是一套由JavaScript语言编写的应用程序接口,可帮助您在网站中构建功能丰富、交互性强的地图应用,支持PC端和移动端基于浏览器的地图应用开发,且支持HTML5特性的地图开发。

MapVGL,是一款基于WebGL的地理信息可视化库,可以用来展示大量基于3D的地理信息点线面数据。设计初衷主要是为了解决大数据量的三维地理数据展示问题及一些炫酷的三维效果。

下面我们采用MapVGL来实现全国空气质量的地图可视化效果。

下面介绍其前端代码实现:

html代码如下:

代码语言:html复制
 <!DOCTYPE html>
 <html lang="zh-CN">
 <head>
    <meta charset="utf-8">
    <title>MapVGL</title>
    <meta http-equiv="X-UA-Compatible" content="IE=Edge">
    <meta name="viewport" content="initial-scale=1.0, user-scalable=no">
    <link href="/static/css/loader.css" rel="stylesheet">
    <style>
        .loader {
            width: 200px;
            height: 200px;
            left: 0;
            top: 0;
            right: 0;
            bottom: 0;

            position: fixed;
            z-index: 99999;
            text-align: revert;
            margin: auto;
        }

        html,
        body {
            width: 100%;
            height: 100%;
            margin: 0;
            padding: 0;
            background: #000;
 
        }

        #map_container {
            width: 100%;
            height: 100%;
            margin: 0;
        }

        .map_grade {
            position: fixed;
            left: 0;
            bottom: 0;
            width: 252px;
            z-index: 1000;
        }

        .map_grade .map_shadowColor {
            display: inline-block;
            width: 100%;
            height: 8px;
            margin-bottom: -5px;
            background-image: linear-gradient(90deg, #43ce17 15%, #e1d72c 22%, #f72d0e 75%, #a7134c 90%);
        }

        .map_grade .map_leveltip {
            color: #fff;
            font-size: 12px;
            border-collapse: collapse;
            border-spacing: 0;
            z-index: 1001;
            background: rgba(0, 0, 0, .56);
        }

        .map_grade .map_leveltip td {
            display: inline-block;
            width: 42px;
            height: 44px;
            text-align: center;
            vertical-align: middle;
            line-height: 40px;
        }

        .map_grade .map_leveltip .map_level {
            background: transparent;
        }


        #mapgroup .radio-button-wrapper {
            width: 80px;
            padding: 0 !important;
            text-align: center;
            border: 0 !important;
            background: rgba(0, 0, 0, .6) !important;
            color: #fff !important;
            border-radius: 0 !important;
        }

        .mapgroup {
            position: fixed;
            right: 0;
            top: 23%;
            z-index: 999;
            width: 80px;
        }

        .radio-group {
            width: 80px;
            display: grid;
            cursor: pointer;
        }

        .white .radio-button,
        .radio-button {
            background-color: white;
            color: #000;

            line-height: 40px;
            text-align: center;

            text-decoration: none;
            border: 1px solid black;
            border-radius: 10px;
            caret-color: transparent;
        }

        .dark .radio-button {
            background-color: black;
            color: #fff;

            line-height: 40px;
            text-align: center;

            text-decoration: none;
            border: 1px solid white;
            border-radius: 10px;
            caret-color: transparent;
        }

        .radio-button-checked,
        .white .radio-button-checked,
        .dark .radio-button-checked {
            background-color: #337ab7;
            color: #fff;
            caret-color: transparent;
        }

        .timetip {
            position: fixed;
            left: 51px;
            top: 66px;
            z-index: 401;
            padding: 3px 10px;
            font-size: 15px;
            background: rgba(0, 0, 0, .56);
            color: #fff;
            border-radius: 4px;
        }
    </style>
    <script src="//api.map.baidu.com/api?v=2.0&type=webgl&ak=your baidu map ak"></script>
    <script src="../static/js/common.js"></script>
    <script src="//mapv.baidu.com/build/mapv.min.js"></script>
    <script src="https://code.bdstatic.com/npm/mapvgl@1.0.0-beta.139/dist/mapvgl.min.js"></script>

</head>

<body>

    <div class="loader"></div>

    <div id="map_container"></div>
    <div id="mapgroup" class="mapgroup" contenteditable="false">
        <div class="radio-group"><a class="radio-button radio-button-checked" data-factor="aqi" onclick="javascript:void(0);"><span class="radio-button-inner">AQI</span></a><a class="radio-button" data-factor="pm2_5" onclick="javascript:void(0);"><span class="radio-button-inner">PM2.5</span></a><a class="radio-button" data-factor="pm10" onclick="javascript:void(0);"><span class="radio-button-inner">PM10</span></a> <a class="radio-button" onclick="javascript:void(0);" data-factor="so2"><span class="radio-button-inner">SO2</span></a> <a class="radio-button" onclick="javascript:void(0);" data-factor="no2"><span class="radio-button-inner">NO2</span></a> <a onclick="javascript:void(0);" class="radio-button" data-factor="co"><span class="radio-button-inner">CO</span></a> <a class="radio-button" onclick="javascript:void(0);" data-factor="o3"><span class="radio-button-inner">O3</span></a> </div>
        <div class="radio-group"><a id="toggleLabel" data-label="name" onclick="javascript:void(0);" class="radio-button"><span class="radio-button-inner" style="color: magenta;">显数值</span> </a><a id="toggleStyle" class="radio-button" onclick="javascript:void(0);"><span class="radio-button-inner" style="color: orange;">暗夜系</span> </a> </div>
    </div>

    <div class="map_grade"><canvas class="map_shadowColor"></canvas>
        <table class="map_leveltip">
            <tbody>
                <tr style="white-space: nowrap;">
                    <td class="map_level">优</td>
                    <td class="map_level">良</td>
                    <td class="map_level">轻度</td>
                    <td class="map_level">中度</td>
                    <td class="map_level">重度</td>
                    <td class="map_level">严重</td>
                </tr>
            </tbody>
        </table>
    </div>
    <div class="timetip">
        <div id="timetip"></div>
        <div style="display: none;">数据源暂未更新数据!</div>
    </div>

    <script src="/static/lib/jquery/jquery-1.11.3.js"></script>
    <script src="/static/lib/underscore/underscore.js"></script>
    <script src="/static/lib/moment/moment.min.js"></script>
    <script src="/static/lib/moment/zh-cn.js"></script>
    <script src="/static/lib/crypto/crypto.js"></script>
    
    <script src="/static/js/mapv.js"></script>
</body></html>

common.js定义了地图的风格样式以及初始化地图函数

代码语言:javascript复制
/**
 * 初始化地图
 */

/* global BMapGL */
/* global darkStyle */

function initMap(options) {
    options = Object.assign({
        tilt: 60,
        heading: 0
    }, options);
    var map = new BMapGL.Map('map_container', {
        restrictCenter: false,
        style: {styleJson: options.style || darkStyle }
    });
    map.enableKeyboard();
    map.enableScrollWheelZoom();
    map.enableInertialDragging();
    map.enableContinuousZoom();

    map.setDisplayOptions(options.displayOptions || {
            indoor: false,
            poi: true,
            skyColors: options.skyColors || [
                'rgba(5, 5, 30, 0.01)',
                'rgba(5, 5, 30, 1.0)'
            ]
        });
    if (options.center && options.zoom) {
        map.centerAndZoom(new BMapGL.Point(options.center[0], options.center[1]), options.zoom);
    }

    map.setTilt(options.tilt);
    map.setHeading(options.heading);
    return map;
}



var darkStyle = [{
    featureType: 'background',
    elementType: 'geometry',
    stylers: {
        color: '#070c17ff'
    }
},  ......
];

var whiteStyle = [{
    featureType: 'water',
    elementType: 'geometry',
    stylers: {
        visibility: 'on',
        color: '#ccd6d7ff'
    }
}, ......];

核心js代码mapv.js如下:

代码语言:javascript复制
 
args=_.object(window.location.search.replace('?', '').split('&').map(x => x.split('=')));
 
var whiteBox=[
                    // 地面颜色
                    'rgba(226, 237, 248, 0)',
                    // 天空颜色
                    'rgba(186, 211, 252, 1)'
                ]; 
function initMapV() {
    var options = {
        tilt: 56,
        heading: 0.3,
        center: [113, 34],
        zoom: 9,
        style:whiteStyle,  
        skyColors:whiteBox

    };
    var hour=(new Date).getHours();
    if(hour>18||hour<6){
        options['style'] = darkStyle;
        delete options['skyColors'];
        $('body').addClass('dark');
        $('#toggleStyle span').text('白日风');
        
    }
    
    if (args['style'] == 'white') {
        
        options['style'] = whiteStyle;
        options['skyColors'] = whiteBox;
        $('body').removeClass('dark').addClass('white');
        $('#toggleStyle span').text('暗夜风');
    } else if(args['style'] == 'dark'){
        options['style'] = darkStyle;
        delete options['skyColors'];
        $('body').addClass('dark');
        $('#toggleStyle span').text('白日风');
    }
    map = initMap(options);


    points_station = [];
    points_city = [];
    texts_city = [];
    texts_station = [];


    citys.filter(x => x['stations']).forEach(x => {

        var cityCenter = mapv.utilCityCenter.getCenterByCityName(x['name']);
        var point_city = {
            geometry: {
                type: 'Point',
                coordinates: [cityCenter.lng, cityCenter.lat]
            },
            properties: {
                name: x['name']
            }
        };
        var text_city = {
            geometry: {
                type: 'Point',
                coordinates: [cityCenter.lng, cityCenter.lat]
            },
            properties: {
                name: x['name'],
                text: x.name,
                textColor: '#000',

                backgroundColor: '#fff',
            },
        };
        points_city.push(point_city);
        texts_city.push(text_city);
        x['stations'].forEach(y => {


            var point_station = {
                geometry: {
                    type: 'Point',
                    coordinates: [y.lng, y.lat]
                },
                properties: {
                    name: y.name,
                }
            };
            var text_station = {
                geometry: {
                    type: 'Point',
                    coordinates: [y.lng, y.lat],
                },
                properties: {
                    name: y.name,
                    text: y.name,
                    textColor: '#000',

                    backgroundColor: '#fff',
                },
            };

            points_station.push(point_station);
            texts_station.push(text_station);
        });

    });

    view = new mapvgl.View({
        
        map: map
    });

    cityLayer = new mapvgl.PointLayer({
        blend: 'lighter',
        size: 15,
        color: 'rgba(227, 29, 120, 0.6)'
    });

    view.addLayer(cityLayer);
    cityLayer.setData(points_city);
    view.hideLayer(cityLayer);
    stationLayer = new mapvgl.PointLayer({
        blend: 'lighter',
        size: 15,
        color: 'rgba(102, 0, 204, 0.6)'
    });

    stationLayer.setData(points_station);
    view.addLayer(stationLayer);
    
    stationTextLayer = new mapvgl.LabelLayer({
        textAlign: 'center',

        borderColor: '#fff',
        borderWidth: 0,
        padding: [2, 5],
        borderRadius: 5,
        fontSize: 12,
        lineHeight: 16,
        collides: true, // 是否开启碰撞检测, 数量较多时建议打开
        enablePicked: true,
        onClick: e => {
            // 点击事件
            console.log('click', e);
        },
    });
    stationTextLayer.setData(texts_station);
    view.addLayer(stationTextLayer);
    
    cityTextLayer = new mapvgl.LabelLayer({
        textAlign: 'center',
        borderColor: '#fff',
        borderWidth: 0,

        padding: [2, 5],
        borderRadius: 5,
        fontSize: 12,
        lineHeight: 16,
        collides: true, // 是否开启碰撞检测, 数量较多时建议打开
        enablePicked: true,
        onClick: e => {
            // 点击事件
            console.log('click', e);
        },
    });

    cityTextLayer.setData(texts_city);
    view.addLayer(cityTextLayer);
    view.hideLayer(cityTextLayer);
    map.on('zoomend', function () {
        var currentZoom = map.getZoom();
        if (currentZoom > toggleZoom) {
            view.showLayer(stationLayer);
            view.showLayer(stationTextLayer);
            view.hideLayer(cityLayer);
            view.hideLayer(cityTextLayer);
        } else {
            view.showLayer(cityLayer);
            view.showLayer(cityTextLayer);
            view.hideLayer(stationLayer);
            view.hideLayer(stationTextLayer);
        }
    });
}
var b = new Base64();
toggleZoom = 10;
async function loaddata(time) {

    toggleZoom = 8;
    method = "GETREALTIMEMAP";
    var points = [];

    var obj = {
        type: 'aqi',
    };
    var param = getParam(method, obj);
    return $.ajax({
        type: "post",
        url: "/api",
        data: {
            param: param
        },
        success: function (data) {

        }
    });
}
loaddata=_.memoize(loaddata);
dictRange = {

    'so2': [-1, 0, 150, 500, 650, 800,800,800,800],

    'no2': [-1, 0, 100, 200, 700, 1200, 2340, 3090, 3840],
    'pm10': [-1, 0, 50, 150, 250, 350, 420, 500, 600],

    'co': [-1, 0, 5, 10, 35, 60, 90, 120, 150],
    'o3': [-1, 0, 160, 200, 300, 400, 800, 1000, 1200],

    'pm2_5': [-1, 0, 35, 75, 115, 150, 250, 350, 500],

    'aqi': [-1, 0, 50, 100, 150, 200, 300, 400, 500],

};
_.mapObject(dictRange,(v,k)=>{v.pop();v.pop();v.push(Infinity)});
levelColors = ["#808080aa", "#43ce17aa", "#efdc31aa", "#ffaa00aa", "#ff401aaa", "#d20040aa", "#9c0a4eaa"];

function refreshMapV() {

    var mode = $('#toggleLabel').data('label');
    var factor = $('.radio-group [data-factor].radio-button-checked').data('factor');
    loaddata((new Date).getHours()).then(function (data) {
        if(data.search('Warning')>=0){
            data = data.split('<br />n')[4]; 
        }
       
        data = eval('('   b.decode(decryptData(data))   ')');
        console.info(data);
        global_province = data.result.data.province.rows;
        global_city = data.result.data.city.rows;
        $('#timetip').text(`数据时间: ${global_city[0].time.slice(0,-3)}`);
        
        var dict_province = _.object(global_province.map(x => {
            return [x['provincename'], x]
        }));
        var dict_city = _.object(global_city.map(x => {
            return [x['cityname'], x]
        }));

        points_city.map(x => {
            var name = x.properties['name'];
            var properties = dict_province[name];
            var colorIndex = _.sortedIndex(dictRange[factor],  properties[factor]);
            var color = levelColors[colorIndex - 1].slice(0, 7);
            x['properties'] = {
                ...properties,
                ...{
                    'name': name
                },
                ...{
                    'color': color,
                }

            };
        });

        texts_city.map(x => {
            var name = x.properties['name'];

            var properties = dict_province[name];
            var value =  properties[factor];
            var text = (mode == 'value' ? value >> 0 : name).toString();
            var colorIndex = _.sortedIndex(dictRange[factor], value);
            var color = levelColors[colorIndex - 1].slice(0, 7);
            x['properties'] = {
                ...properties,
                ...{
                    'name': name
                },
                ...{
                    'text': text,
                    'backgroundColor': color,
                    'textColor': colorIndex < 5 ? '#000' : '#fff',
                }

            };
        });

        points_station.map(x => {
            var name = x.properties['name'];

            var properties = dict_city[name];
            var colorIndex = _.sortedIndex(dictRange[factor],  properties[factor]);
             
            var color = levelColors[colorIndex - 1].slice(0, 7);
            x['properties'] = {
                ...properties,
                ...{
                    'name': name
                },
                ...{
                    'color': color,
                }

            };
        });

        texts_station.map(x => {
            var name = x.properties['name'];

            var properties = dict_city[name];
            var value =  properties[factor];
            var text = (mode == 'value' ? value >> 0 : name).toString();
            var colorIndex = _.sortedIndex(dictRange[factor], value);
            var color = levelColors[colorIndex - 1].slice(0, 7);
            x['properties'] = {
                ...properties,
                ...{
                    'name': name
                },
                ...{
                    'text': text,
                    'backgroundColor': color,
                    'textColor': colorIndex < 5 ? '#000' : '#fff',
                }

            };
        });

        cityLayer.setData(points_city);
        cityTextLayer.setData(texts_city);

        stationLayer.setData(points_station);
        stationTextLayer.setData(texts_station);
        if (mode == 'none') {
            view.removeLayer(cityTextLayer);
            view.removeLayer(stationTextLayer);
             
        } else {
             
            view.addLayer(cityTextLayer);
            view.addLayer(stationTextLayer);
            
        }
    });
}

$(function () {
    
    $('#toggleStyle').click(function () {
         var text=$(this).find('span').text();
         location.href =(text == '白日风') ? location.pathname '?style=white' : location.pathname '?style=dark';
    })

    
    $('#toggleLabel').click(function () {
        var labelType = $(this).data('label');
        if (labelType == 'none') {
            $(this).data('label', 'name');
            $(this).find('span').text('显数值');
        }
        if (labelType == 'value') {
            $(this).data('label', 'none');
            $(this).find('span').text('显名字');
        }
        if (labelType == 'name') {
            $(this).data('label', 'value');
            $(this).find('span').text('无标注');
        }
        refreshMapV();

    });
    $('.radio-group [data-factor]').click(function () {
        $('.radio-group [data-factor]').removeClass('radio-button-checked');
        $(this).addClass('radio-button-checked');
        refreshMapV();
    })
    loaddata((new Date).getHours()).then(function (data) {
        if(data.search('Warning')>=0){
            data = data.split('<br />n')[4]; 
        }
        data = eval('('   b.decode(decryptData(data))   ')');
        console.info(data);
        data = data.result.data;
        citys = _.chain(data.city.rows).groupBy('provincename').map((x, i) => {
            return {
                'code': i,
                'name': i,
                'stations': x.map(y => {
                    return {
                        'code': y['cityname'],
                        'name': y['cityname'],
                        'lng': y['longitude'],
                        'lat': y['latitude']

                    }
                })
            }
        }).value()
        initMapV();
        refreshMapV();
        $('.loader').remove();
        setInterval(refreshMapV,1000*60*10);
       
    })
})

0 人点赞