mapbox地图添加 echarts 环形图 和 柱状图

652 阅读3分钟

image.png image.png

1. 封装加载地图资源组件

loadMap 方法用于初始化地图, 配置地图图层, 再调用添加数据源和图层的方法

addSource 方法用于添加数据源

addLayer 方法用于添加图层

addMarker 用于将echart 和柱状图定位到地图

import jingzhou_county from './geojson-county.json'
import jingzhou_city from './geojson-city.json'

declare const mapboxgl: any;

const useMap = () => {
    let _mapbox: any;
    // 资源集合
    const allsources: string[] = [];
    // 图层集合
    const allLayers: string[] = [];
    // 打点集合
    const allMarkers: string[] = [];
    // 弹窗集合
    const allPopup: string[] = [];
    
    /**
     * 加载地图要素
     * @param {Function} callback 回调函数
     */
    const loadMap = (map, callback: Function = () => { }) => {
        _mapbox = map._mapboxLayer._mapbox
        const sources = { jingzhou_county, jingzhou_city }
        // 1.加载地图资源
        // 1.1遍历资源
        for (const key in sources) {
            // 1.2加载地图资源
            addSource(key, sources[key]);
        }

        // 图层配置
        const LayerData = [
            //  区县填充
            {
                id: 'jingzhou_county_fill',
                type: 'fill',
                source: 'jingzhou_county',
                layout: {
                    visibility: 'visible',
                },
                paint: {
                    'fill-color': '#165DFF', //#EEBF15
                    'fill-opacity': 0.2,
                },
            },
            //  区县轮廓
            {
                id: 'jingzhou_county',
                type: 'line',
                source: 'jingzhou_county',
                layout: {
                    'line-join': 'round',
                    'line-cap': 'round',
                },
                paint: {
                    'line-color': '#fff',
                    'line-width': ['interpolate', ['linear'], ['zoom'], 1, 1, 22, 5],
                    'line-opacity': 0.8,
                    'line-dasharray': [2, 4]
                },
            },
            //  高亮区县轮廓
            {
                id: 'jingzhou_county_highLight',
                type: 'line',
                source: 'jingzhou_county',
                layout: {
                    'line-join': 'round',
                    'line-cap': 'round',
                },
                paint: {
                    'line-color': '#0d99ff',
                    'line-width': ['interpolate', ['linear'], ['zoom'], 1, 1, 22, 5],
                    'line-opacity': 1,
                },
                filter: ['in', 'name', ''],
            },
            //  市区轮廓
            {
                id: 'jingzhou_city',
                type: 'line',
                source: 'jingzhou_city',
                layout: {
                    'line-join': 'round',
                    'line-cap': 'round',
                },
                paint: {
                    'line-color': '#fff',
                    'line-width': ['interpolate', ['linear'], ['zoom'], 1, 1, 22, 5],
                    'line-opacity': 0.8,
                },
            },
            //  市区轮廓偏移
            {
                id: 'jingzhou_city_offset',
                type: 'line',
                source: 'jingzhou_city',
                layout: {
                    'line-join': 'round',
                    'line-cap': 'round',
                },
                paint: {
                    'line-color': '#165DFF',
                    'line-width': ['interpolate', ['linear'], ['zoom'], 1, 1, 22, 20],
                    'line-opacity': 1,
                    'line-offset': ['interpolate', ['linear'], ['zoom'], 1, -0.5, 22, -10],
                },
            },

        ];
        // 2.遍历图层配置添加图层
        LayerData.forEach((item) => {
            addLayer(item.id, item, callback);
        });
    };


    /**
     * 加载资源
     * @param {any} featuresData feature数据
     * @param {string} sourceName 资源名称
     */
    const addSource = (sourceName: string, featuresData: any) => {
        // 1. 记录资源添加
        if (!allsources.includes(sourceName)) {
            allsources.push(sourceName);
        }
        // 2. 判断_mapbox是否已有资源
        if (!_mapbox.getSource(sourceName)) {
            _mapbox.addSource(sourceName, {
                type: 'geojson',
                data: featuresData,
            });
        } else {
            // 3. 替换资源
            (_mapbox.getSource(sourceName) as any).setData(featuresData);
        }
    };

    /**
     * 加载图层
     * @param {string} layerName 图层名称
     * @param {mapboxgl.AnyLayer} layerData 图层参数
     * @param {Function} callback 回调函数
     */
    const addLayer = (layerName: string, layerData: any, callback: Function) => {
        // 判断是否有数据源
        if (!_mapbox.getSource(layerData.source)) {
            return;
        }
        // 0. 记录图层添加
        if (!allLayers.includes(layerName)) {
            allLayers.push(layerName);
        }
        // 1.判断是否已有图层,没有才添加
        if (!_mapbox.getLayer(layerName)) {
            // 2.添加图层
            _mapbox.addLayer(layerData);
            if (layerName.includes('_fill')) {
                const filterLayerName = layerName.replace('_fill', '_highLight')
                // 3.添加鼠标移入事件
                _mapbox.on('mouseenter', layerName, (e) => {
                    _mapbox.getCanvas().style.cursor = 'pointer';
                    if (e.features && e.features[0]) {
                        // 获取区县名称
                        const name = e.features[0].properties.name;
                        _mapbox.setFilter(filterLayerName, ['in', 'name', name]);
                    }
                });
                // 4.添加鼠标移除事件
                _mapbox.on('mouseleave', layerName, () => {
                    _mapbox.getCanvas().style.cursor = '';
                    _mapbox.setFilter(filterLayerName, ['in', 'name', '']);
                });
            }

        }
    };
    /**
     * 加载marker点
     * @param {any} el 
     * @param {number[]} coordinate 坐标
     */
    const addMarker = (el, coordinate: number[]) => {
        // 实例化marker, marker打环形图跟柱状图会偏
        const marker = new mapboxgl.Marker({
             anchor: 'center',
             element: el,
             scale: 1
        }).setLngLat(coordinate).addTo(_mapbox);

        if (!allMarkers.includes(marker)) {
            allMarkers.push(marker);
        }
    };
    /**
     * 弹窗popup
     * @param {any} el 
     * @param {number[]} coordinate 坐标
     */
    const addPopup = (el, coordinate: number[]) => {
        const popup = new mapboxgl.Popup({
            anchor: 'center',
            className: 'popup-container',
            closeButton: false,
            closeOnClick: false,
            
        }).setLngLat(coordinate)
            .setDOMContent(el)
            .addTo(_mapbox);

        if (!allPopup.includes(popup)) {
            allPopup.push(popup);
        }
    };
    /**
     * 设置图层显示隐藏
     * @param {type} 参数
     * @returns {type} 返回值
     */
    const setLayerVisible = (show: boolean) => {
        //_mapbox && _mapbox.setPitch(show ? 45 : 0) // 设置地图倾角
        allLayers.forEach((layer: any) => {
            _mapbox.setLayoutProperty(layer, 'visibility', show ? 'visible' : 'none');
        })
        allPopup.forEach((popup: any) => {
            show ? popup._setOpacity(1) : popup._setOpacity(0)
        })
    }
    return { loadMap, addMarker, addPopup }
}

export default useMap

2. 封装echart加载方法

initCharts 方法用于初始化echarts容器

addCharts 方法用于添加配置生成echarts

import { ref, markRaw } from 'vue';
import * as echarts from 'echarts';
import county from './geojson-county.json'

const useEcharts = () => {

    // echart 实例
    const myChart = ref<any>(null);

    /**
     * 初始化echarts
     */
    const initCharts = (el) => {
        // 移除之前创建的实例并且重新创建一个Echarts实例达到刷新效果
        el.removeAttribute('_echarts_instance_');
        myChart.value = markRaw(echarts.init(el));
    };
    /**
     * 添加charts
     */
    const addCharts = (el, option) => {
        initCharts(el)
        myChart.value.setOption(option);
    };

    return {
        myChart,
        initCharts,
        addCharts,
    }
}

export default useEcharts

3. 调用地图和echarts 的vue组件

加载地图比onMounted还慢, 用了setInterval来监听地图加载完

环形图用的 echarts 来生成

柱状图直接用html + css 来生成比较简单

<template>
    <div class="map-echarts">
        <template v-for="item in jingzhou_county.features" :key="item">
            <div v-if="level === 'fibrilUsageRate'" :style="{ transform: scaleValue }"
                :class="[item.properties.className, 'echarts-circle']"></div>
            <div v-else-if="level !== '网架概览'" v-show="showColumn" :style="{ transform: scaleValue }"
                :class="[item.properties.className, 'echarts-column']">
                <div class="column-box"
                    :style="{ 'height': (Number((item.properties as any)[level]) * percent[level] + 5) + 'px' }">
                </div>
                <div class="column-text">{{ item.properties.name.slice(0, 2) + '供电所' }}</div>
                <div class="column-num">{{ (item.properties as any)[level] }}</div>
            </div>

        </template>
    </div>
</template>

<script setup lang="ts">
import { ref, watch, nextTick } from 'vue';
import useEcharts from './useEcharts'
import useMap from './useMap'
import jingzhou_county from './geojson-county.json'
import { mapListStore } from '@/store/mapList'
import 'echarts-liquidfill'

const mapList = mapListStore()
const { loadMap, addMarker, setLayerVisible } = useMap()
const { addCharts } = useEcharts()

const props = defineProps<{ level: string, tableData: any }>()

// 显示柱状图
const showColumn = ref(false)
// 缩放比例
const scaleValue = ref('scale(0.8)')
// 柱状图比例
const percent = { branchLength: 0.1, poleTotal: 0.05, fiberSwitchTotal: 1, }



watch(() => props.level, (newVal) => {
    if (newVal === '' || newVal === '网架概览') {
        setLayerVisible(false)
        return
    }

    nextTick(() => {
        showColumn.value ? setLayerVisible(true) : initMap()

        // 加载marker
        updateMarkers()
    })
}, { immediate: true })


watch(() => props.tableData, (newVal) => {
    jingzhou_county.features.forEach((item: any) => {
        const properties = item.properties
        let obj = newVal.find(v => v.name.includes(properties.name.slice(0, 2))) || {}
        item.properties = {
            ...properties,
            branchLength: obj.branchLength || 0, // 线路总长度
            fiberSwitchTotal: obj.fiberSwitchTotal || 0, // 接头盒总数
            poleTotal: obj.poleTotal || 0,// 杆塔总数
            fibrilUsageRate: obj.fibrilUsageRate || 0,// 纤芯使用率
        }
    })
})

/**
 * 初始化地图
 * @param {type} 参数
 * @returns {type} 返回值
 */
const initMap = () => {
    const map = mapList.allMapData['decisionAnalysis']
    loadMap(map)
    // 地图加载完, 柱状图才显示
    showColumn.value = true
    // 监测地图缩放移动
    map.on('zoom-complete', () => {
        if (props.level === '网架概览') return
        const zoom = map.getZoom()
        scaleValue.value = `scale(${1 + (zoom - 10) * 0.2})`
        updateMarkers()
    }, 'decisionAnalysis')
}

/**
 * 地图缩放更新marker
 * @param {type} 参数
 * @returns {type} 返回值
 */
const updateMarkers = () => {
    // 遍历区县
    jingzhou_county.features.forEach(item => {
        const properties: any = item.properties
        let el = document.querySelector('.' + properties.className)
        if (props.level === 'fibrilUsageRate') {
            // 添加环形图echarts
            handleAddCharts(el, parseFloat(properties.fibrilUsageRate) / 100)
        }
        addMarker(el, properties.centroid)
    })
}


/**
* 添加环形图
* @param {type} 参数
* @returns {type} 返回值
*/
const handleAddCharts = (el, percent) => {
    const option = {

        series: [{
            type: 'liquidFill',
            radius: '90%',
            data: [percent],
            label: {
                normal: {
                    color: '#1D2129',
                    insideColor: '#fff',
                    textStyle: {
                        fontSize: 16,
                    }
                }
            },
            color: [{
                type: 'linear',
                x: 0,
                y: 1,
                x2: 0,
                y2: 0,
                colorStops: [{
                    offset: 1,
                    color: ['#165DFF'], // 0% 处的颜色
                }, {
                    offset: 0,
                    color: ['#9FF9FF'], // 100% 处的颜色
                }],
                global: false // 缺省为 false
            }],
            outline: {
                show: true,
                borderDistance: 2,
                itemStyle: {
                    borderColor: '#165DFF',
                    borderWidth: 2
                }
            }
        }]
    };

    addCharts(el, option)
}



</script>

<style lang="scss" scoped>
.echarts-circle {
    width: 50px;
    height: 50px;
    border-radius: 50%;
    background-color: #fff;
}

.echarts-column {
    @include flex(3);
    flex-direction: column;

    .column-box {
        width: 8px;
        height: 50px;
        background: linear-gradient(180deg, #165DFF 0%, #9FF9FF 100%);
        box-shadow: inset 0px 4px 4px rgba(0, 0, 0, 0.25);
        border-radius: 2px;
    }

    .column-text {
        font-size: 14px;
        color: #4E5969;
    }

    .column-num {
        font-size: 16px;
        color: #1D2129;
    }
}
</style>
<style lang="scss">
.popup-container {
    .mapboxgl-popup-content {
        box-shadow: unset !important;
        background: transparent;
    }
}
</style>