Canvas draws a map

In our large-screen visualization projects, map data visualization is the most common function. There are many current implementations of map data visualization, the most representative of which is the use of echarts, the introduction of a js file, and some simple configurations, so that a map is displayed. But as an excellent front-end programmer, for many technologies, we need to know what it is and why it is so. This chapter takes the Canvas technology in HTML5 as the background, and briefly explains some ideas for the visualization of map data. I hope It can bring you some inspiring thinking, and the specific implementation effect is as follows: 

The implementation steps of map data visualization are as follows:

  1. Query and download map data files, generally geojson files, world maps, Chinese maps, and administrative unit maps are all available
  2. The page acquires and parses the geojson file. In order to fill the specified area with the map, it is necessary to calculate the bounding box range of the map, calculate the latitude and longitude of the center point, and calculate the zoom factor of the map
  3. Traverse the map file data, convert the latitude and longitude coordinates into screen coordinates, combine the Canvas to draw polygon related APIs, and map the location information on the canvas according to the zoom factor
  4. Response to mouse event processing, for Canvas, is actually redrawing. Two APIs are mainly used here—isPointInPath or isPointInStroke to determine whether the mouse click or hover area is on the specified area (difficulty)

Reference material: geojson map latitude and longitude coordinate range

Next, let’s enter the main text—the code. The page layout structure is as follows:

<canvas id="container"></canvas>

 The relevant styles are as follows:

* {
    margin: 0;
    padding: 0;
}

html,
body {
    width: 100%;
    height: 100%;
}

canvas {
    display: block;
    width: 100%;
    height: 100%;
}

 The specific implementation logic is as follows:

let canvas = document.querySelector('#container')
let canvasW = canvas.width = window.innerWidth
let canvasH = canvas.height = window.innerHeight
let geoCenterX = 0, geoCenterY = 0  // 地图区域的经纬度中心点
let scale = 1   // 地图缩放系数
let geoData = []
let offsetX = 0, offsetY = 0    // 鼠标事件的位置信息
let eventType = ''  // 事件类型
let ctx = canvas.getContext('2d')

// 地图绘制入口方法
function init() {
    let request = new XMLHttpRequest()
    request.open('get', 'https://geo.datav.aliyun.com/areas_v3/bound/410000_full.json')
    //request.open('get', './henan.json')
    request.send()
    request.onload = function () {
        if (request.status === 200) {
            geoData = JSON.parse(request.responseText)
            getBoxArea()
            drawMap()
        }
    }
}

// 分三步,清空画布、绘制地图各子区域、标注城市名称
function drawMap() {
    ctx.clearRect(0, 0, canvasW, canvasH)
    // 画布背景
    ctx.fillStyle = '#000'
    ctx.fillRect(0, 0, canvasW, canvasH)
    drawArea()
    drawText()
}

// 绘制地图各子区域
function drawArea() {
    let dataArr = geoData.features
    let cursorFlag = false
    for (let i = 0; i < dataArr.length; i++) {
        let centerX = canvasW / 2
        let centerY = canvasH / 2
        dataArr[i].geometry.coordinates.forEach(area => {
            ctx.save()
            ctx.beginPath()
            ctx.translate(centerX, centerY)
            area[0].forEach((elem, index) => {
                let position = toScreenPosition(elem[0], elem[1])
                if (index === 0) {
                    ctx.moveTo(position.x, position.y)
                } else {
                    ctx.lineTo(position.x, position.y)
                }
            })
            ctx.closePath()
            ctx.strokeStyle = '#00cccc'
            ctx.lineWidth = 1
            // 将鼠标悬浮的区域设置为橘黄色
            if (ctx.isPointInPath(offsetX, offsetY)) {
                cursorFlag = true
                ctx.fillStyle = 'orange'
                if (eventType === 'click') {
                    console.log(dataArr[i])
                }
            } else {
                ctx.fillStyle = '#004444'
            }
            ctx.fill()
            ctx.stroke()
            ctx.restore()
        });
        // 动态设置鼠标样式
        if (cursorFlag) {
            canvas.style.cursor = 'pointer'
        } else {
            canvas.style.cursor = 'default'
        }
    }
}
// 标注地图上的城市名称
function drawText() {
    let centerX = canvasW / 2
    let centerY = canvasH / 2
    geoData.features.forEach(item => {
        ctx.save()
        ctx.beginPath()
        ctx.translate(centerX, centerY) // 将画笔移至画布的中心
        ctx.fillStyle = '#fff'
        ctx.font = '16px Microsoft YaHei'
        ctx.textAlign = 'center'
        ctx.textBaseLine = 'center'
        let x = 0, y = 0
        //  因不同的geojson文件中中心点属性信息不同,这里需要做兼容性处理
        if (item.properties.cp) {
            x = item.properties.cp[0]
            y = item.properties.cp[1]
        } else if (item.properties.centroid) {
            x = item.properties.centroid[0]
            y = item.properties.centroid[1]
        } else if (item.properties.center) {
            x = item.properties.center[0]
            y = item.properties.center[1]
        }
        let position = toScreenPosition(x, y)
        ctx.fillText(item.properties.name, position.x, position.y);
        ctx.restore()
    })
}

// 将经纬度坐标转换为屏幕坐标
function toScreenPosition(horizontal, vertical) {
    return {
        x: (horizontal - geoCenterX) * scale,
        y: (geoCenterY - vertical) * scale
    }
}

// 获取包围盒范围,计算包围盒中心经纬度坐标,计算地图缩放系数
function getBoxArea() {
    let N = -90, S = 90, W = 180, E = -180
    geoData.features.forEach(item => {
        // 将MultiPolygon和Polygon格式的地图处理成统一数据格式
        if (item.geometry.type === 'Polygon') {
            item.geometry.coordinates = [item.geometry.coordinates]
        }
        // 取四个方向的极值
        item.geometry.coordinates.forEach(area => {
            let areaN = - 90, areaS = 90, areaW = 180, areaE = -180
            area[0].forEach(elem => {
                if (elem[0] < W) {
                    W = elem[0]
                }
                if (elem[0] > E) {
                    E = elem[0]
                }
                if (elem[1] > N) {
                    N = elem[1]
                }
                if (elem[1] < S) {
                    S = elem[1]
                }
            })
        })
    })
    // 计算包围盒的宽高
    let width = Math.abs(E - W)
    let height = Math.abs(N - S)
    let wScale = canvasW / width
    let hScale = canvasH / height
    // 计算地图缩放系数
    scale = wScale > hScale ? hScale : wScale
    // 获取包围盒中心经纬度坐标
    geoCenterX = (E + W) / 2
    geoCenterY = (N + S) / 2
}

// 滚轮缩放事件
canvas.addEventListener('mousewheel', function (event) {
    if (event.deltaY > 0) {
        if (scale > 10) {
            scale -= 10
        }
    } else {
        scale += 10
    }
    eventType = 'mousewheel'
    drawMap()
})

// 鼠标移动事件
canvas.addEventListener('mousemove', function (event) {
    offsetX = event.offsetX
    offsetY = event.offsetY
    eventType = 'mousemove'
    drawMap()
})

// 鼠标点击事件
canvas.addEventListener('click', function (event) {
    offsetX = event.offsetX
    offsetY = event.offsetY
    eventType = 'click'
    drawMap()
})

init()

Friends who have finished reading, if it is helpful to your work, remember to like it, your encouragement will be the motivation for the author to continue to create, come on! !

Guess you like

Origin blog.csdn.net/qq_40289557/article/details/123074104