Canvas绘制地图

2023-05-16

在我们的大屏可视化项目中,地图数据可视化是最常见功能。地图数据可视化目前的实现方案有很多,其中最具有代表性的莫过于使用echarts,引入一个js文件,再加上一些简单的配置,这样一个地图就展示出来了。但是作为一名优秀的前端程序员,对于很多技术,我们既要知其然,也要只其所以然,本章节以HTML5中的Canvas技术为背景,简单讲解一下地图数据可视化实现的一些思路,希望能够给大家带来一些具有启发性的思考,具体实现效果如下图: 

地图数据可视化的实现步骤如下:

  1. 查询并下载地图数据文件,一般是geojson文件,世界地图、中国地图、行政单位地图均可
  2. 页面获取并解析geojson文件,为了让地图能充满指定区域,需要计算一下地图的包围盒范围并计算中心点经纬度,并且计算地图的缩放系数
  3. 遍历地图文件数据,将经纬度坐标转换为屏幕坐标,结合Canvas绘制多边形相关api,根据缩放系数将位置信息映射在画布上
  4. 响应鼠标事件处理,针对Canvas来说,其实就是重绘。这里主要用到了两个api——isPointInPath或isPointInStroke判断鼠标点击或悬浮区域是否在指定的区域上(难点)

参考资料:geojson地图经纬度坐标范围

接下来就开始进入正文——撸代码,页面布局结构如下:

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

 相关样式如下:

* {
    margin: 0;
    padding: 0;
}

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

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

 具体实现逻辑如下:

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()

看完的小伙伴,如果对你工作有帮助,记得点个赞,你的鼓励将是作者不断创作的动力,加油!!! 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Canvas绘制地图 的相关文章

随机推荐

  • 目标跟踪之Pysot系列代码训练(SiamRPN\SiamRPN++)

    代码地址 xff1a https github com STVIR pysot 环境配置参考博客 xff1a Siam系列跟踪算法工具包PySOT配置 一 数据集准备 Pysot系列跟踪器训练的时候 xff0c 首先将数据集进行裁剪 xff
  • 基于Paddle实现实例分割

    百度的Paddle这几年发展十分迅速 xff0c 而且文档十分齐全 xff0c 涉及到机器视觉的多个应用领域 xff0c 感觉还是非常牛的 xff0c 各种backbone xff0c 损失函数 数据增强手段以及NMS等 xff0c 整体感
  • 机器学习笔记: 时间序列 分解 STL

    1 前言 STL Seasonal and Trend decomposition using Loess 是以LOSS 作为平滑方式的时间序列分解 LOSS可以参考机器学习笔记 xff1a 局部加权回归 LOESS UQI LIUWJ的博
  • C++11 auto遍历

    C 43 43 11这次的更新带来了令很多C 43 43 程序员期待已久的for range循环 xff0c 每次看到javascript xff0c lua里的for range xff0c 心想要是C 43 43 能有多好 xff0c
  • C++ 文件的读写(fin && fout)

    如何让键盘输入字符保存在 txt文件中 如何让我们自己在键盘上输入的字符不仅仅在屏幕上显示 xff0c 而且还能保存在特定路径的文件中 xff0c 这让简单枯燥的控制台命令程序变得略有趣 首先 xff0c 先看看cin和cout对象 xff
  • 基本矩阵、本质矩阵和单应矩阵

    两幅视图存在两个关系 xff1a 第一种 xff0c 通过对极几何一幅图像上的点可以确定另外一幅图像上的一条直线 xff1b 另外一种 xff0c 通过上一种映射 xff0c 一幅图像上的点可以确定另外一幅图像上的一个点 xff0c 这个点
  • 矩阵零空间

    矩阵A的零空间就Ax 61 0的解的集合 零空间的求法 xff1a 对矩阵A进行消元求得主变量和自由变量 xff1b 给自由变量赋值得到特解 xff1b 对特解进行线性组合得到零空间 假设矩阵如下 xff1a 对矩阵A进行高斯消元得到上三角
  • VIO学习总结

    VIO xff08 visual inertial odometry xff09 即视觉惯性里程计 xff0c 有时也叫视觉惯性系统 xff08 VINS xff0c visual inertial system xff09 xff0c 是
  • 单应性(Homography)变换

    我们已经得到了像素坐标系和世界坐标系下的坐标映射关系 xff1a 其中 xff0c u v表示像素坐标系中的坐标 xff0c s表示尺度因子 xff0c fx fy u0 v0 xff08 由于制造误差产生的两个坐标轴偏斜参数 xff0c
  • senmantic slam mapping

    basicStructure hpp common h 定义一些常用的结构体 以及各种可能用到的头文件 xff0c 放在一起方便include 相机内参模型 增加了畸变参数 xff0c common headers h各种可能用到的头文件
  • Ubuntu 20.04 VNC 安装与设置

    原链接 VNC是一个远程桌面协议 按照本文的说明进行操作可以实现用VNC对Ubuntu 20 04进行远程控制 一般的VNC安装方式在主机没有插显示器的时候是无法使用的 下面的操作可以在主机有显示器和没有显示器时都能够正常工作 首先安装x1
  • opencv中类型转换问题

    记录一下最近困惑我的问题 方便以后查阅 在学习立体匹配算法中BM算法时 xff0c 出现在了关于类型转换的问题 xff1a disp convertTo disp8u CV 8U 255 numberOfDisparities 16 不知道
  • 最大似然估计MLE与贝叶斯估计

    最大似然估计 Maximum Likehood Estimation MLE 最大似然估计的核心思想是 xff1a 找到参数 的一个估计值 xff0c 使得当前样本出现的可能性最大 用当年博主老板的一句话来说就是 xff1a 谁大像谁 xf
  • 大疆Livox_mid 40雷达初体验

    为了解决无人车上镭神雷达FOV小而导致的车前3m内无法看到锥形桶问题 东家给公司邮寄了一台大疆的mid40雷达 不得不说 颜值真的高 光看颜值 就甩镭神几条街 昨天重新配置镭神的激光雷达 官方给的配置软件 真的是 用的我心碎啊 算了 不提了
  • 地铁供电系统的构成

    地铁供电系统一般划分为以下几部分 xff1a 外部电源 xff1b 主变电所 xff1b 牵引供电系统 xff1b 动力照明系统和杂散电流腐蚀防护系统 xff1b 电力监控系统 外部电源地铁供电系统的外部电源就是地铁供电系统主变电所供电的外
  • C++ Vector常用函数

    C 43 43 Vector常用函数 begin 函数 原型 xff1a iterator begin const iterator begin 功能 xff1a 返回一个当前vector容器中起始元素的迭代器 end 函数 原型 xff1
  • STM32使用ADC获取内部温度传感器数据输出(直接读取/DMA两种方式实现)

    STM32使用ADC获取内部温度传感器数据输出 xff08 直接读取 DMA两种方式实现 xff09 前言一 内部温度传感器的使用 xff1f 二 代码操作讲解1 直接读取2 DMA处理 总结 前言 STM32F1系列 xff08 本代码基
  • fp32/fp64精度,4/8字节16进制转float/double十进制

    1 IEEE 754 32位单精度浮点数 xff08 4字节 xff09 1 1 32位单精度浮点数 其中 xff0c 32位16进制数包括1位符号位 SIGN xff0c 8位指数位 EXPONENT 和 23位尾数位 MANTISSA
  • SVG绘制圆环进度条

    在我们的大屏可视化项目中 xff0c 经常会用到各种各样的图表 与传统的表格展示 枯燥的文字阐述相比 xff0c 图表展示则使用户看起来更加直观 数据的展示则更加一目了然 本文基于svg绘图技术结合前端技术栈vue xff0c 以工作中常用
  • Canvas绘制地图

    在我们的大屏可视化项目中 xff0c 地图数据可视化是最常见功能 地图数据可视化目前的实现方案有很多 xff0c 其中最具有代表性的莫过于使用echarts xff0c 引入一个js文件 xff0c 再加上一些简单的配置 xff0c 这样一