简介:基于HTML5 Canvas与ECharts构建的交互式地图绘制项目,面向具备一定前端基础、希望深入学习地图可视化与GeoJSON数据应用的开发者。项目利用Canvas的路径、填充等API绘制地图基础轮廓,再通过ECharts地图系列加载GeoJSON地理数据,并借助httpgeojson.io在线工具自由调整省市边界,实现可自定义的交互地图,非常适合用于地理数据分析、区域业务监控及个性化地图展示等场景。压缩包整体约1.1MB,内容紧凑,便于快速下载学习。目前已有1630人浏览学习。通过学习该资源,读者可掌握Canvas画布的基本绘图方法,理解如何将地理坐标转为屏幕像素;可熟悉ECharts地图系列的配置与Roam、缩放等交互选项;还能学会解析和应用GeoJSON中的几何信息,并据此实现地图缩放、平移、点击下钻等联动效果,为搭建更复杂的地理信息可视化应用打下扎实基础。
1. 用 H5 Canvas 画地图,为什么还离不开 geojson.io
很多第一次拆这份源码包的同事都问我同一个问题:地图不都是拿 Leaflet、Mapbox 这类库画的吗,为什么偏要用 Canvas 自己画?我的回答很简单——当你要画的是几百米范围内的厂区导览、门店分布图,或者干脆是一张画布上的示意路线图时,引入整套瓦片地图库是杀鸡用牛刀。H5 Canvas 方案加载更快、样式完全可控,配合 geojson.io 这个在线绘制工具,地图数据完全不用后端参与:你在网页地图上随便点几个点、拉几条线、圈一块区域,导出成 GeoJSON,前端读取后直接画在画布上,整条链路清爽得很。
这份资源适合两类人:一类是做 H5 可视化、运营活动页、轻量地图场景的前端,另一类是手上已有 GeoJSON 数据、想自己控制渲染细节、不想被地图 SDK 绑定的从业者。它把“在线画地图 → 导出数据 → Canvas 渲染 → 点击交互”这条路完整跑通了。下面我按自己拆包时的顺序,把这个方案从数据到绘制的每个环节讲清楚。
2. 数据从哪来:用 geojson.io 把地图画成 GeoJSON
如果连数据都没有,Canvas 绘制能力再强也是空转。geojson.io 这个工具的价值在于,把“地图数据制作”这件事从命令行和文本编辑器里解放出来——你不需要手写一个几十万字符的 JSON 文件,只需要在网页地图上点几下,右侧面板会自动同步生成对应的 GeoJSON 结构。
2.1 geojson.io 操作路径与 GeoJSON 坐标规则
打开 geojson.io 后,页面左侧是地图底图,右侧是实时刷新的 JSON 面板。工具栏里最常用的三个按钮分别是画点(Marker)、画线(LineString)和画面(Polygon)。点一下画点按钮,在地图上任意位置单击,右侧就会多出一个Feature;画线按钮需要连续单击多个位置,双击结束;画面按钮同理,围出一块区域后双击自动闭合。
这一步最容易翻车的地方在坐标顺序。GeoJSON 规范里coordinates的数组顺序固定是[经度, 纬度],也就是[lng, lat],不是[lat, lng]。geojson.io 上画出来的点,鼠标悬浮能看到经纬度数字,比如[116.3912, 39.9075],前面是经度,后面是纬度。这个顺序你在后面写 Canvas 绘制代码时如果调换了,点就会画到地图外或者对称翻转的位置,属于新手最容易踩的坑之一。
画完之后,左侧面板底部有导出按钮,可以下载.geojson文件;也可以直接复制右侧的 JSON 内容,本地新建一个map.geojson文件粘贴进去,效果完全一样。反过来,geojson.io 也支持把本地 GeoJSON 文件直接拖进页面,拖进去之后地图上会立刻显示出内容,方便你拿真实数据处理。
2.2 GeoJSON 要素结构:properties 与 geometry 怎么配合 Canvas
geojson.io 导出的内容是标准的FeatureCollection。我在实际使用中给每个要素的properties约定几个字段:name表示这个点的名称,type表示渲染类型(poi画圆点、route画路线、area画区域),color控制颜色。这样 Canvas 绘制时可以直接从properties里读样式,不用再套一张映射表。
下面这个是我拆包后随手画的一张示意园区图,包含一个点、一条线和一个面:
{ "type": "FeatureCollection", "features": [ { "type": "Feature", "properties": { "name": "北门", "type": "poi", "color": "#e74c3c" }, "geometry": { "type": "Point", "coordinates": [116.3912, 39.9075] } }, { "type": "Feature", "properties": { "name": "主路", "type": "route", "color": "#3498db", "lineWidth": 4 }, "geometry": { "type": "LineString", "coordinates": [ [116.3912, 39.9075], [116.3930, 39.9068], [116.3951, 39.9062] ] } }, { "type": "Feature", "properties": { "name": "A 区", "type": "area", "color": "#2ecc71", "fill": "rgba(46, 204, 113, 0.25)" }, "geometry": { "type": "Polygon", "coordinates": [ [ [116.3912, 39.9075], [116.3930, 39.9068], [116.3951, 39.9062], [116.3912, 39.9075] ] ] } } ] }注意看上面三种几何类型在数据层级上的区别:Point的coordinates是一维数组;LineString是二维数组,每个元素是一个点;Polygon是三维数组,最外层数组里可以有多组闭合环,第一个环是外边界,后面的环是内洞。大多数场景只用到coordinates[0]这一组外环就够。这组结构理解了,后面的绘制代码就能逐个分支处理。
2.3 经纬度怎么变成像素坐标:投影与范围归一化
经纬度是球面角度,Canvas 是平面像素坐标,中间必须做一层投影换算。对于小区、园区这类几公里范围的地图,没必要上完整的 UTM 投影,我用得最多的是两种方案的组合:整体范围小就做“等距近似”,直接用参考纬度做余弦修正;范围大到省级以上就跑一遍 Web Mercator。
Web Mercator 的核心公式很短,我在资源里的map.js中写的简化版本是这样:
const EARTH_HALF_CIRCUMFERENCE = 20037508.34; function toMercator(lng, lat) { const x = (lng / 180) * EARTH_HALF_CIRCUMFERENCE; const rad = (lat * Math.PI) / 180; const y = Math.log(Math.tan(Math.PI / 4 + rad / 2)) / (Math.PI / 180) * (EARTH_HALF_CIRCUMFERENCE / 180); return { x, y }; }这段代码把经纬度换算成以米为单位的平面坐标。逻辑上是标准 Web Mercator 投影,x用经度线性映射,y用对数变换保证纬度越高压缩越厉害。参数EARTH_HALF_CIRCUMFERENCE是地球周长的一半,单位是米,用于把角度换算成实际距离,数值精度对屏幕展示完全够用。
拿到所有要素的投影坐标之后,再计算这批坐标的包围盒(bounding box),也就是所有点里x和y的最小值、最大值。然后用最小值和最大值做归一化,把坐标映射到0 ~ canvasWidth和0 ~ canvasHeight的范围。这一步的目的是让地图撑满画布,而不是缩在某个角落。还有一个细节不能漏:Canvas 的y轴向下,而投影后的纬度高在数值上更大,所以映射时要把y轴翻转,否则画出来的园区是上下颠倒的。
3. 把 GeoJSON 画上 Canvas:DPR、投影与三种几何类型的绘制
作为前端绘制的核心,这一章直接落到代码。整个渲染器我习惯拆成三段独立的逻辑:第一步做画布初始化与高分屏适配,第二步把 GeoJSON 坐标转换成像素坐标,第三步按几何类型逐要素绘制。
3.1 画布初始化:先解决高分屏模糊
第一步是初始化 Canvas。这个细节如果漏掉,后面所有绘制在 iPhone 和 Retina 屏上都会发虚——因为 CSS 像素和物理像素在手机上是 2 倍甚至 3 倍的关系,Canvas 画布像素数不够时浏览器的放大算法不会给你做抗锯齿处理。
function setupCanvas(canvas, cssWidth, cssHeight) { const dpr = window.devicePixelRatio || 1; canvas.width = Math.floor(cssWidth * dpr); canvas.height = Math.floor(cssHeight * dpr); canvas.style.width = cssWidth + 'px'; canvas.style.height = cssHeight + 'px'; const ctx = canvas.getContext('2d'); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); return ctx; }这里的dpr是设备的像素比,普通电脑显示器是 1,iPhone 通常是 2 或 3。代码先按cssWidth * dpr设置画布的真实分辨率,再用ctx.setTransform(dpr, 0, 0, dpr, 0, 0)做一个放大变换。这样后面绘制时,所有坐标仍然按 CSS 像素的数值来写,比如画布宽 600,你写x = 300就落在正中央,不用去乘dpr。这个习惯我每次起步都会写死进脚手架,省得后面补返工。
3.2 坐标管道:从经纬度数组到像素坐标
初始化完画布,下一个核心是坐标转换管道。我习惯把投影、包围盒计算、像素映射封装成一个computeView函数,一次性算出所有要素共用的视图参数:
function computeView(geojson, canvasWidth, canvasHeight, padding) { const points = []; const walkCoords = (coords) => { if (typeof coords[0] === 'number') { points.push(toMercator(coords[0], coords[1])); } else { coords.forEach((c) => walkCoords(c)); } }; geojson.features.forEach((feature) => walkCoords(feature.geometry.coordinates)); const xs = points.map((p) => p.x); const ys = points.map((p) => p.y); const minX = Math.min(...xs); const maxX = Math.max(...xs); const minY = Math.min(...ys); const maxY = Math.max(...ys); return { minX, maxX, minY, maxY, scale: Math.min( (canvasWidth - padding * 2) / (maxX - minX || 1), (canvasHeight - padding * 2) / (maxY - minY || 1) ), offsetX: padding, offsetY: padding, canvasWidth, canvasHeight }; } function toPixel(lng, lat, view) { const m = toMercator(lng, lat); const x = (m.x - view.minX) * view.scale + view.offsetX; const y = view.canvasHeight - ((m.y - view.minY) * view.scale + view.offsetY); return [x, y]; }computeView里的walkCoords用递归方式兼容了 Point、LineString、Polygon 三种坐标层级,思路是先判断当前数组第一个元素是不是数字,是数字就说明这个数组是一个[lng, lat]点,否则继续往里递归。这样后面再加 MultiPolygon 这种嵌套类型也能复用。scale取宽度和高度两个缩放系数里较小的那个,保证地图在画布里既撑满又不会被拉伸变形。toPixel里那一行y的计算就是前面提过的 Y 轴翻转,先算出归一化位置,再用画布高度减掉它。
3.3 三种几何类型的绘制:样式从 properties 里读
坐标转换就绪,接下来的绘制逻辑就比较直接了。一个drawMap函数按要素类型分三种情况处理,样式统一从feature.properties里读取:
function drawMap(canvas, geojson, options = {}) { const cssWidth = options.width || canvas.clientWidth; const cssHeight = options.height || canvas.clientHeight; const ctx = setupCanvas(canvas, cssWidth, cssHeight); const view = computeView(geojson, cssWidth, cssHeight, options.padding || 40); ctx.clearRect(0, 0, cssWidth, cssHeight); geojson.features.forEach((feature) => { const { type, coordinates } = feature.geometry; const props = feature.properties || {}; ctx.beginPath(); if (type === 'Point') { const [x, y] = toPixel(coordinates[0], coordinates[1], view); ctx.arc(x, y, props.radius || 6, 0, Math.PI * 2); ctx.fillStyle = props.color || '#e74c3c'; ctx.fill(); ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 2; ctx.stroke(); } else if (type === 'LineString') { coordinates.forEach((coord, i) => { const [x, y] = toPixel(coord[0], coord[1], view); i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); }); ctx.strokeStyle = props.color || '#3498db'; ctx.lineWidth = props.lineWidth || 4; ctx.lineJoin = 'round'; ctx.lineCap = 'round'; ctx.stroke(); } else if (type === 'Polygon') { coordinates[0].forEach((coord, i) => { const [x, y] = toPixel(coord[0], coord[1], view); i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); }); ctx.closePath(); ctx.fillStyle = props.fill || 'rgba(52,152,219,0.25)'; ctx.fill(); ctx.strokeStyle = props.color || '#2c3e50'; ctx.lineWidth = props.lineWidth || 2; ctx.stroke(); } if (props.name) { const [x, y] = toPixel( type === 'Point' ? coordinates[0] : coordinates[0][0], type === 'Point' ? coordinates[1] : coordinates[0][1], view ); ctx.fillStyle = '#333333'; ctx.font = '12px sans-serif'; ctx.fillText(props.name, x + 8, y - 8); } }); return view; }绘制逻辑的关键点是每次循环开头都要ctx.beginPath(),否则前一个要素的路径会和当前要素叠在一起,描边出现多余的连线。Point用arc画圆,LineString走moveTo/lineTo并用lineCap、lineJoin让拐角平滑,Polygon用closePath闭合路径后先填充再描边。最后如果properties.name存在,就在几何中心点旁边用fillText写一个标签。fillText的 y 坐标减 8,是为了让文字整体位于点的上方。
这一步完成后,你已经能在浏览器里看到一张完整的地图了。动手验证的方法很简单:把geojson变量替换成 geojson.io 导出的内容,打开index.html,如果画的点和线位置关系跟你在 geojson.io 上画的一致,说明投影和翻转都没问题。
4. H5 场景接入:点击命中、尺寸变化与页面适配
绘制只是第一步,实际放在 H5 里跑的时候,还要处理页面适配和交互。这里有一个很容易被忽略的认知:Canvas 画完是一张静态位图,浏览器不知道“北门”在哪块像素区域,要点击命中必须自己写几何判断。
4.1 三种接入姿态:普通网页、uniapp H5 与 App 内嵌 WebView
普通网页是最简单的用法,index.html里放一个<canvas id="mapCanvas">,然后在script里调用上面的drawMap。适合放在管理后台、运营活动页,甚至作为 H5 可视化编辑器的画布底座。
uniapp 场景下有个区别要特别注意:uni 的uni.createCanvasContext是一个异步队列模型,绘制指令不是立刻执行到 Canvas 上的。如果你用 H5 端的<canvas type="2d">,拿到的其实是原生 Canvas 节点,可以脱离 uni 的封装直接操作。我一般这样区分:
// #ifdef H5 const canvas = document.getElementById('mapCanvas'); const ctx = canvas.getContext('2d'); drawMap(canvas, geojson); // #endif // #ifndef H5 const ctx = uni.createCanvasContext('mapCanvas', this); ctx.setFillStyle('#ffffff'); ctx.fillRect(0, 0, 300, 200); ctx.draw(); // #endif#ifdef H5是 uniapp 的条件编译语法。H5 端我直接走原生 Canvas 分支,这样可以复用drawMap的全部逻辑,包括 DPR 适配和命中检测;小程序端才退回到uni.createCanvasContext的队列式 API。这个取舍的核心原因是:队列式 Canvas 在频繁重绘时容易丢帧,而且拿不到像素级的事件坐标,交互要做二次封装代价很高。
App 内嵌 H5 的原理和普通网页一致,只是 WebView 的devicePixelRatio可能是 2 或 3,代码里window.devicePixelRatio || 1会自动适配,不需要额外处理。
4.2 点击命中:用射线法判断多边形内外
点击事件拿到的是屏幕 CSS 像素坐标,要判断点中了哪个要素,最直接的办法是把要素投影到屏幕坐标再做命中检测。线型要素比较特殊,需要计算点到折线的最小距离是否小于一个阈值;点要素只需要算距离;面要素则用经典的射线法判断点是否在多边形内部。
射线法代码不长,我把它原样贴在下面:
function isPointInPolygon(px, py, polygonPixels) { let inside = false; for (let i = 0, j = polygonPixels.length - 1; i < polygonPixels.length; j = i++) { const [xi, yi] = polygonPixels[i]; const [xj, yj] = polygonPixels[j]; const intersect = (yi > py) !== (yj > py) && px < ((xj - xi) * (py - yi)) / (yj - yi) + xi; if (intersect) inside = !inside; } return inside; } canvas.addEventListener('click', (e) => { const rect = canvas.getBoundingClientRect(); const px = e.clientX - rect.left; const py = e.clientY - rect.top; const hitFeature = geojson.features.find((feature) => { if (feature.geometry.type === 'Polygon') { const pixels = feature.geometry.coordinates[0].map((c) => toPixel(c[0], c[1], view)); return isPointInPolygon(px, py, pixels); } return false; }); if (hitFeature) { console.log('选中要素:', hitFeature.properties.name); } });射线法的原理是:从目标点向右侧发一条水平射线,统计这条射线与多边形边的交点个数,奇数次说明点在多边形内部,偶数次说明点在外部。上面intersect那行同时判断了边的两个端点是否在点击点两侧,以及点击点是否在边的左侧,满足条件的边才会计数。这个算法处理凹多边形也没问题,是图形学里最通用的命中方案之一。点的半径命中就按像素距离小于半径处理,线则用点到线段距离的近似计算,需要容忍度时把阈值调到 4~6 像素比较顺手。
4.3 尺寸变化与重绘:resize、旋转与键盘弹起
移动端页面最烦的就是布局抖动。用户旋转屏幕、浏览器地址栏收起、App 内嵌页里点 input 弹键盘,这些场景都会触发resize事件。每次 resize 之后都要重新计算画布宽高,重建投影视图,再调用一次绘制。
let resizeTimer = null; window.addEventListener('resize', () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(() => { view = drawMap(document.getElementById('mapCanvas'), geojson, { width: window.innerWidth, height: window.innerHeight }); }, 300); });300 毫秒的防抖是为了避免键盘弹起过程中连续触发多次重绘。有一次我在 App 里调试时发现,键盘弹出会导致 WebView 高度瞬间变小再恢复,如果每次触发都重绘,画布会在屏幕上闪烁。加防抖之后这个问题基本消失。绘制函数返回的view对象建议保存到全局变量,因为resize和点击命中都要复用同一份视图参数,避免新建投影导致的坐标偏移。
5. 避坑清单:坐标顺序、坐标系偏移与 iOS 白图
这段是拆包过程中积累的血泪经验,一共五条,覆盖我从数据制作到移动端渲染遇到的最典型的翻车现场。每一条我都按“现象 → 原因 → 解决”的顺序写清楚。
5.1 地理要素画到了地图外面:坐标顺序写反
现象:在 geojson.io 上画好的点,传到自己的页面里出现在完全错误的位置,甚至跑到画布外;画出来的线整体镜像翻转。
原因:GeoJSON 的坐标顺序是[经度, 纬度],很多人习惯性认为纬度在前,用了[lat, lng]去绘制。经纬度两个数字一旦对调,一个在 [39.9, 116.4] 范围的点会变成 [116.4, 39.9],已经是另一块区域了。
解决:在所有坐标入口处统一收口。我在toPixel函数里固定把coordinates[0]当经度、coordinates[1]当纬度,不接受其他顺序。排查时先拿一个已知坐标的单点做冒烟测试,比如北京[116.3912, 39.9075],绘制后确认点的位置在北京附近再继续。
5.2 地图整体偏移几十米:WGS84 与 GCJ02 混用
现象:geojson.io 上画好的边界线引入页面后,点和线整体往东南方向偏移了几十米,跟底图对不上。
原因:geojson.io 的底图基于 WGS84 坐标系,而国内大多数地图 SDK(高德、腾讯)使用 GCJ02 坐标系,两者存在几十米的系统偏移。在很多需要叠加定位的场景里,这种偏移足以让标注落在错误的位置上。
解决:做法取决于你的底图来源。如果底图是 GPS 轨迹或海外地图,直接保留 WGS84;如果底图是国内商业平台,则需要在导入前做坐标系转换。最稳妥的方式是在数据侧用坐标转换算法把 GeoJSON 里所有顶点从 WGS84 转成 GCJ02 再交付给前端。注意 Canvas 本身不关心你用什么坐标系,它只按投影公式计算,坐标系不一致的锅不应该由投影函数来背。
5.3 iPhone 上画布模糊,导出图片还是白图
现象:同一份代码在电脑上绘制清晰锐利,换到 iPhone 上文字边缘发虚;调用canvas.toDataURL()导出图片后一片空白。
原因:前者是典型的未适配devicePixelRatio,iPhone 的 DPR 是 2 或 3,画布实际像素不够,浏览器只能拉伸模糊显示。后者则有两个可能:一是 Canvas 尺寸超过 iOS Safari 的最大面积限制,超过后绘图上下文直接失效;二是画布内容来源跨域污染了画布,toDataURL被安全策略禁止。
解决:适配 DPR 用 3.1 节的setupCanvas即可;导出白图先检查画布面积,iOS 上画布总面积不要超过 16777216 像素(约等于 4096×4096),超出就分块绘制或按需缩略导出。如果你是在 uniapp 里导出,还需要确认绘制指令已经通过ctx.draw()的 callback 真正输出完,再调导出接口。
5.4 uniapp H5 用 Canvas 队列绘制时白图
现象:在 uniapp 项目里调用uni.createCanvasContext绘制地图,H5 端页面显示空白,甚至canvasToTempFilePath导出的图片是纯白背景。
原因:uni.createCanvasContext在 H5 端实现为一串异步队列,setFillStyle、fillRect、draw这些指令只是入队,要等draw的绘制回调完成后才能确认已上屏。如果在尚未完成时就去截图导出,截到的自然是一张空白画布。真的复杂地图场景下,队列式 Canvas 也不适合承载大规模绘制。
解决:H5 端一律绕过 uni 的封装,直接拿原生 Canvas 上下文来画。代码在 4.1 节已经给了条件编译的写法。如果你的项目必须走 uni API,那就把所有绘制指令放在一个ctx.draw(false, callback)的 callback 里,导出操作放进 callback 中执行,每一步都确认上屏后再做下一步。
5.5 App 内嵌 H5 弹键盘后点击位置偏移
现象:App 壳里的 H5 页面,输入框聚焦弹起键盘后,再点击 Canvas 上的区域,命中的要素和眼睛看到的位置对不上,整体往下偏。
原因:键盘弹出会改变 WebView 的可视高度,导致页面被压缩且滚动位移。Canvas 的getBoundingClientRect()返回值会跟随布局变化,而点击事件里计算坐标时如果用的是缓存值或未考虑滚动位移,两者就会错位。
解决:在点击监听里不要用缓存坐标,直接从事件对象取clientX/clientY,并在当次事件内实时调用canvas.getBoundingClientRect()来计算相对位置。同时监听resize事件,按 4.3 节的防抖逻辑重绘画布,让画面坐标和点击坐标始终基于同一套布局数据。如果你需要 Canvas 不随键盘移动,把 canvas 容器设为position: fixed也能绕开一部分位移问题。
6. 进阶:离屏 Canvas 做颜色拾取命中与一条验证链路
前面 4.2 节的射线法适合点选和面选,但如果是几十个密密麻麻的小区域,在线段上进行距离判断的计算量会明显上升。工程上更常用的是颜色拾取法:按要素把每个区域的填充色整成独一无二的 RGBA 值,不直接画在主画布上,而是绘制到一个同尺寸的离屏 Canvas。用户点击时,只需要在离屏 Canvas 上取点击处一个像素的颜色,反查对应要素即可。
键值映射我用一张普通的对象表维护:
function buildIdColorMap(features) { const map = {}; features.forEach((feature, index) => { const r = (index + 1) & 0xff; const g = ((index + 1) >> 8) & 0xff; const b = ((index + 1) >> 16) & 0xff; map[`${r},${g},${b}`] = feature.properties.name; }); return map; }渲染离屏画布时把每个要素的fillStyle替换成这段编码颜色,再将globalAlpha设为 1;点击监听里用getImageData(x, y, 1, 1).data拿到 RGBA,拼成字符串就能立即命中。这个方法跨点、线、面统一,几条线叠在一起也能正确命中靠上的那一条,是我在复杂 H5 地图场景里的默认候选方案。
最后分享一个我每次都会做的验证步骤:在点击事件里把屏幕坐标反算回经纬度,和 geojson.io 上的原始坐标做对照。
function pixelToLngLat(px, py, view) { const mx = (px - view.offsetX) / view.scale + view.minX; const my = (view.canvasHeight - py - view.offsetY) / view.scale + view.minY; const lng = (mx / EARTH_HALF_CIRCUMFERENCE) * 180; const rad = Math.atan(Math.exp((my / EARTH_HALF_CIRCUMFERENCE) * 180 * (Math.PI / 180))); const lat = (rad - Math.PI / 4) * 2 / (Math.PI / 180); return [lng, lat]; }点击画布上任意一点,控制台打印反算结果,再回到 geojson.io 里搜索这个经纬度,两边相差不超过 0.0001 度说明整条链路的数据复现没有问题。这套 rar 包里的代码,我拆下来后已经多次用在门店分布和园区导览的 H5 页面上。从那以后,我每次接手前端地图任务都会先确认三件事:坐标是不是[lng, lat]、底图坐标系是什么、DPR 适配做了没有。希望帮到你。
本文还有配套的精品资源,点击获取