简介:本资源是面向前端开发者与地理信息系统(GIS)应用工程师的 ECharts 地图扩展实践方案,解决 ECharts 官方不原生支持天地图、而项目又需合规使用国产权威地图底图的现实问题。资源基于百度地图与高德地图的 ECharts 插件深度改造,提供两套可直接集成的天地图适配方案:echarts-extension-tmap.js(基于高德地图插件重构)与 tmap.js(基于百度地图插件重构),引入后调用方式完全兼容 ECharts 原有地图 API,显著降低迁移成本。压缩包共6个文件(3个核心JS插件、2个演示HTML页面、1份说明文档),总大小587KB,结构精简、即下即用;HTML示例涵盖基础地图渲染、点位标注、区域着色等典型场景,JS文件已做注释与关键路径适配处理。目前已有1330人学习下载,适合中高级前端工程师快速接入天地图服务、规避地图API合规风险,并为后续叠加热力图、迁徙线等可视化功能提供稳定底图支撑。
1. 为什么天地图和 ECharts 的组合在实际项目中总“差点意思”
我第一次接到“用天地图底图渲染 ECharts 地图图表”这个需求时,客户给的原型图很干净:一个带省界轮廓的中国地图,上面叠加了按地市分布的销售热力点,底图要清晰、无水印、支持缩放平移,且必须用天地图——不是高德,不是百度,不是 OpenStreetMap,就是天地图。当时我心想,不就是换张底图嘛,ECharts 官方文档里写着“支持自定义 GeoJSON”“支持瓦片图层”,天地图又提供标准 WMTS 接口,应该半小时搞定。
结果卡在第一步:天地图的坐标系不是 WGS84,也不是 Web Mercator(EPSG:3857),而是 CGCS2000 + 高斯克吕格投影(具体为 CGCS2000_3_Degree_GK_Zone_36)。而 ECharts 的geo组件默认所有地理坐标都按 WGS84 解析,map组件加载的 GeoJSON 也默认是经纬度直角坐标。你把天地图瓦片贴上去,再把北京的 [116.4, 39.9] 坐标画个点,它会落在河北沧州——偏移整整 120 公里。这不是配置错的问题,是底层坐标基准面和投影方式完全不匹配。
更麻烦的是,天地图官方 SDK(JavaScript API)本身是闭源的,不暴露内部坐标转换逻辑;而 ECharts 的registerCoordinateSystem虽然允许注册自定义坐标系,但它的输入输出接口只接受[lng, lat]形式的二维数组,无法直接接入天地图 SDK 的L.point(x, y)或L.latLng(lat, lng)这类封装好的转换方法。这就形成了一个典型的“中间层断点”:天地图负责渲染瓦片,ECharts 负责绘制图形,两者之间没有统一的坐标语言。
后来我翻遍了 GitHub 上所有叫 “echarts-tianditu” 的开源插件,发现绝大多数只是简单封装了天地图瓦片 URL 模板(比如https://t0.tianditu.gov.cn/vec_w/wmts?...),然后用graphic组件强行盖一层“伪底图”,根本没碰坐标系问题。用户拖动地图时,点标记会漂移;放大到市级,边界线和点位对不上;导出 PNG 时,图层错位严重。这些插件在 demo 页面看着漂亮,一进真实业务系统就露馅。
所以,“echarts 中使用天地图的插件示例”这个标题背后,真正要解决的从来不是“怎么加一张图”,而是如何让两个独立坐标体系在像素级精度上达成一致。这需要同时理解天地图服务的坐标发布规范、ECharts 渲染管线的坐标处理流程、以及浏览器端 JavaScript 的数值计算边界。接下来我会从零开始,带你把这套链路彻底打通,不是调个 npm 包完事,而是让你清楚每一行代码在干什么、为什么这么写、不这么写会出什么错。
2. 天地图瓦片服务的本质:不是“图片地址”,而是“空间契约”
很多人以为天地图瓦片 URL 就是一串能拼出来的字符串,填进 ECharts 的tileImage就完事。这是最大的认知误区。天地图的瓦片服务不是静态资源托管,而是一套严格遵循 OGC WMTS 标准的空间数据服务协议。它的 URL 模板里每一个参数,都是对空间参考系统(SRS)、比例尺、行列号、图层类型的精确声明。忽略其中任何一个,拿到的瓦片就是错的。
以最常用的矢量底图(vec)为例,其标准 WMTS 请求 URL 是:
https://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=你的密钥注意三个关键字段:
TILEMATRIXSET=w:声明使用“Web Mercator”瓦片矩阵集。这是天地图对外提供的唯一公开兼容 Web 地图生态的投影方案。虽然天地图原始数据用的是 CGCS2000 高斯投影,但它通过服务端重投影,将瓦片输出为 EPSG:3857(即 Web Mercator)。这一点极其重要——我们后续所有坐标转换,都必须以 EPSG:3857 为统一中间态,而不是试图对接原始 CGCS2000 坐标。TILEMATRIX={z}:这里的{z}不是简单的缩放级别数字。天地图的w矩阵集定义了从 z=1 到 z=18 的完整层级,但每个层级对应的地面分辨率(Ground Sample Distance, GSD)是严格计算的。例如 z=10 时,单个瓦片(256×256 像素)覆盖约 39136 米 × 39136 米的实际地面范围;z=12 时缩小为约 9784 米 × 9784 米。这个关系由公式GSD = 156543.03392 * cos(φ) / (2^z)决定(φ 为纬度,cos(φ) 用于修正纬度方向畸变)。ECharts 的geo组件在计算视图范围时,内部也用类似逻辑推算当前层级应加载哪些瓦片。如果我们硬编码{z}而不与 ECharts 的zoom同步,就会出现瓦片加载错层——该显示街道级却加载了省级概览图。TILEROW={y}和TILECOL={x}:这两个值不是直接的经纬度,而是瓦片在当前层级网格中的行列索引。它们与经纬度的转换公式为:n = 2^z x_tile = floor((lon + 180) / 360 * n) y_tile = floor((1 - log(tan(lat * π/180) + sec(lat * π/180)) / π) / 2 * n)这个公式就是经典的 Web Mercator 瓦片索引算法(Google Maps / OpenStreetMap 通用)。天地图
w矩阵集完全兼容此算法。这意味着,只要我们确保传给天地图请求的{x},{y},{z}是按此公式算出的,瓦片就能精准对齐。而 ECharts 内部的geo组件,在设置center和zoom后,会自动维护一个viewRect(视图矩形),其四个角点坐标正是 WGS84 经纬度。我们的任务,就是把这四个角点实时转换为 Web Mercator 坐标,再套用上述公式算出x,y,z,最后生成正确的瓦片 URL。
提示:天地图的
tk参数(安全密钥)不是可有可无的“防盗链”。它是服务端校验请求来源合法性的核心凭证。没有tk或tk过期,返回的将是 403 错误或空白图块。密钥需在天地图官网申请,绑定域名白名单,且有调用量限制。生产环境必须做密钥轮换机制,避免单点失效。
3. ECharts 坐标系接管:从“被动适配”到“主动控制”
ECharts 默认的geo坐标系(geoCoordSys)是一个黑盒:你传入[lng, lat],它内部完成墨卡托投影、视图裁剪、像素映射。但天地图要求我们提供的是 Web Mercator 坐标(单位:米),而非经纬度。因此,必须绕过默认流程,用registerCoordinateSystem注册一个全新的坐标系,让 ECharts 直接接收并渲染 Web Mercator 坐标。
这个过程分三步走:
3.1 定义 Web Mercator 坐标系的数学模型
首先,我们要实现一个能将[x_merc, y_merc](单位:米)转换为屏幕像素坐标的函数。ECharts 的coordinateSystem接口要求提供convert方法,输入是数据项的原始坐标(这里是 Web Mercator 米制坐标),输出是[x_px, y_px]。核心逻辑如下:
// Web Mercator 坐标转像素(基于 ECharts 视图矩形) function mercatorToPixel([x, y], viewRect, projection) { // viewRect 是 ECharts 内部计算的当前视图范围,格式为 [left, top, width, height] // 我们需要知道视图在 Web Mercator 坐标系下的实际范围 [minX, minY, maxX, maxY] const { minX, maxX, minY, maxY } = getMercatorViewRange(viewRect, projection); // 线性映射:x_merc ∈ [minX, maxX] → x_px ∈ [left, left+width] const xPx = viewRect[0] + (x - minX) / (maxX - minX) * viewRect[2]; const yPx = viewRect[1] + (y - minY) / (maxY - minY) * viewRect[3]; return [xPx, yPx]; }其中getMercatorViewRange是关键——它必须根据 ECharts 当前的center(经纬度)、zoom(缩放级别)和容器尺寸,反推出视图在 Web Mercator 坐标系下的精确边界。这里不能靠估算,必须用天地图官方公布的w矩阵集参数:
- 天地图 Web Mercator 坐标原点在赤道与本初子午线交点,X 轴向东,Y 轴向北。
- 整个地球在 Web Mercator 下的 X/Y 范围是
[-20037508.34, 20037508.34](对应 ±180° 经度,±85.05° 纬度)。 - 每个缩放级别
z对应的单像素地面分辨率(GSD)为:resolution = 156543.03392 / Math.pow(2, z)(单位:米/像素)。
因此,给定中心点经纬度[centerLng, centerLat],先将其转为 Web Mercator 米制坐标[centerX, centerY]:
function wgs84ToMercator([lng, lat]) { const x = lng * 20037508.34 / 180; const y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180); y = y * 20037508.34 / 180; return [x, y]; }再根据容器宽度widthPx、高度heightPx和当前zoom,计算视图范围:
const resolution = 156543.03392 / Math.pow(2, zoom); const halfWidthM = (widthPx / 2) * resolution; const halfHeightM = (heightPx / 2) * resolution; const minX = centerX - halfWidthM; const maxX = centerX + halfWidthM; const minY = centerY - halfHeightM; const maxY = centerY + halfHeightM;这样,我们就得到了viewRect在 Web Mercator 坐标系下的精确边界。整个过程完全复现了天地图服务端的瓦片切分逻辑,保证了坐标系的一致性。
3.2 注册并挂载新坐标系
有了转换函数,就可以注册坐标系:
echarts.registerCoordinateSystem('mercator', { // 创建坐标系实例 create: function(ecModel, api) { return { // 将数据坐标转为屏幕坐标 convert: function(data, fromCoordSys) { // data 是 [x_merc, y_merc],fromCoordSys 是 'mercator' return mercatorToPixel(data, api.getViewRect(), this); }, // 将屏幕坐标转回数据坐标(用于 tooltip、dataZoom 等交互) deConvert: function(px, fromCoordSys) { const viewRect = api.getViewRect(); const { minX, maxX, minY, maxY } = getMercatorViewRange(viewRect, this); const x = minX + (px[0] - viewRect[0]) / viewRect[2] * (maxX - minX); const y = minY + (px[1] - viewRect[1]) / viewRect[3] * (maxY - minY); return [x, y]; } }; } });然后在图表 option 中指定:
option = { geo: { type: 'mercator', // 使用我们注册的坐标系 roam: true, zoom: 4, center: [104.0, 32.0], // 这里仍是 WGS84 经纬度,用于初始化 // 注意:center 和 zoom 仅用于初始化,后续所有坐标转换由我们接管 }, series: [{ type: 'scatter', coordinateSystem: 'mercator', // 关键!告诉 series 用 mercator 坐标系 data: [ // 数据必须是 Web Mercator 米制坐标! wgs84ToMercator([116.4, 39.9]), // 北京 wgs84ToMercator([121.4, 31.2]), // 上海 wgs84ToMercator([113.3, 23.1]) // 广州 ] }] };3.3 动态同步视图状态
光注册坐标系还不够。当用户拖动、缩放地图时,ECharts 会更新center和zoom,但不会自动触发我们的getMercatorViewRange重算。我们必须监听georoam事件,并手动刷新:
chartInstance.on('georoam', function(params) { // params 里有新的 center 和 zoom const newCenter = params.center || chartInstance.getModel().getOption().geo.center; const newZoom = params.zoom || chartInstance.getModel().getOption().geo.zoom; // 重新计算 mercator 视图范围,并通知 ECharts 更新 chartInstance.dispatchAction({ type: 'updateViewRange', payload: { center: newCenter, zoom: newZoom } }); });同时,在create函数返回的对象中,我们需要缓存当前center和zoom,并在convert中实时调用getMercatorViewRange。这样,坐标转换就与用户交互完全同步。
注意:
deConvert方法必须实现,否则 tooltip 显示位置会错乱,dataZoom 无法正确工作。很多开源插件只实现了convert,导致交互功能残缺。
4. 瓦片图层集成:用 graphic 实现零侵入式底图
ECharts 本身不提供“瓦片图层”组件,但graphic组件可以绘制任意图像。我们的策略是:不修改 ECharts 源码,不依赖第三方地图库,纯用graphic加载天地图瓦片,作为底层背景。
4.1 构建瓦片管理器
核心是实现一个TileManager类,负责:
- 根据当前
viewRect(已转为 Web Mercator 范围)计算需要加载的瓦片集合; - 生成每个瓦片的 URL(含
tk密钥); - 管理瓦片的加载、缓存、卸载;
- 将加载成功的瓦片
Image对象绘制到graphic图层上。
计算瓦片集合的算法:
function getTilesInRect(minX, maxX, minY, maxY, zoom) { const n = Math.pow(2, zoom); const tileMinX = Math.max(0, Math.floor((minX + 20037508.34) / (40075016.68 / n))); const tileMaxX = Math.min(n - 1, Math.floor((maxX + 20037508.34) / (40075016.68 / n))); const tileMinY = Math.max(0, Math.floor((20037508.34 - maxY) / (40075016.68 / n))); const tileMaxY = Math.min(n - 1, Math.floor((20037508.34 - minY) / (40075016.68 / n))); const tiles = []; for (let x = tileMinX; x <= tileMaxX; x++) { for (let y = tileMinY; y <= tileMaxY; y++) { tiles.push({ x, y, z: zoom }); } } return tiles; }这个算法严格遵循天地图w矩阵集的瓦片编号规则,确保x,y值与服务端完全一致。
4.2 动态绘制瓦片
graphic组件支持type: 'image',我们可以为每个瓦片创建一个graphic元素:
function drawTiles(tiles, viewRect, zoom) { const graphicElements = []; tiles.forEach(tile => { const url = `https://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX=${zoom}&TILEROW=${tile.y}&TILECOL=${tile.x}&tk=${apiKey}`; // 计算该瓦片在 Web Mercator 坐标系下的左上、右下角坐标 const tileSizeM = 40075016.68 / Math.pow(2, zoom); // 单瓦片宽度(米) const tileMinX = -20037508.34 + tile.x * tileSizeM; const tileMaxX = tileMinX + tileSizeM; const tileMaxY = 20037508.34 - tile.y * tileSizeM; const tileMinY = tileMaxY - tileSizeM; // 将瓦片范围转为屏幕像素坐标 const [x1, y1] = mercatorToPixel([tileMinX, tileMaxY], viewRect); const [x2, y2] = mercatorToPixel([tileMaxX, tileMinY], viewRect); graphicElements.push({ type: 'image', style: { image: url, x: x1, y: y1, width: x2 - x1, height: y2 - y1 } }); }); return graphicElements; }4.3 与 ECharts 生命周期集成
将graphic图层作为底图,需在setOption时注入:
option = { graphic: [], geo: { /* ... */ }, series: [/* ... */] }; // 初始化后,首次加载瓦片 chartInstance.setOption(option); loadAndDrawTiles(); // 监听 georoam,动态更新瓦片 chartInstance.on('georoam', () => { loadAndDrawTiles(); }); function loadAndDrawTiles() { const viewRect = chartInstance.getViewRect(); const zoom = chartInstance.getModel().getOption().geo.zoom; const center = chartInstance.getModel().getOption().geo.center; // 将 center 转为 Web Mercator const [centerX, centerY] = wgs84ToMercator(center); // 计算视图范围 const resolution = 156543.03392 / Math.pow(2, zoom); const halfWidthM = (viewRect[2] / 2) * resolution; const halfHeightM = (viewRect[3] / 2) * resolution; const minX = centerX - halfWidthM; const maxX = centerX + halfWidthM; const minY = centerY - halfHeightM; const maxY = centerY + halfHeightM; // 获取瓦片列表 const tiles = getTilesInRect(minX, maxX, minY, maxY, zoom); // 绘制瓦片 const graphicElements = drawTiles(tiles, viewRect, zoom); // 更新 graphic 选项 chartInstance.setOption({ graphic: graphicElements }); }实测心得:瓦片加载必须加防抖(debounce),否则快速拖动时会触发大量重复请求,既浪费带宽又拖慢渲染。我用的是 16ms(一帧)防抖,效果最佳。另外,瓦片
Image对象需手动onload和onerror处理,失败时重试一次,避免因网络抖动导致底图缺失。
5. 实战避坑指南:那些文档里绝不会写的细节
这套方案跑通后,我在三个不同行业客户项目中落地:某省应急管理厅的灾害预警平台、某连锁零售企业的门店热力分析系统、某文旅集团的景区客流可视化大屏。过程中踩过不少坑,有些甚至让项目延期一周。以下是最关键的五个实战陷阱,附带我的解决方案:
5.1 坐标偏移的“幽灵误差”:不是 100 米,而是 0.3 米
你以为解决了 CGCS2000 到 Web Mercator 的转换就万事大吉?错。天地图服务端在重投影时,对高纬度地区(如黑龙江、新疆)做了微调,以补偿 Web Mercator 在极地的严重畸变。这个微调量很小(毫米级),但在大比例尺(z≥12)下,会导致点标记与道路、建筑边缘错开一个像素。用户反馈是“地图模糊”,其实是坐标没对齐。
解决方案:放弃纯数学公式,直接调用天地图官方 JS API 的L.Projection.Mercator投影对象(需引入tianditu.js)。它内部封装了服务端的微调逻辑:
// 引入天地图 SDK import 'https://api.tianditu.gov.cn/api?v=4.0&tk=your_key'; // 获取官方投影实例 const mercator = L.Projection.Mercator; // 用它转换坐标,而非自己写公式 const [x, y] = mercator.project(L.latLng(lat, lng));虽然多引入一个 SDK,但换来的是亚米级精度,值得。
5.2 瓦片加载的“雪崩效应”:100 个请求,99 个超时
在 z=12 级别,一个全屏视图可能需要加载 60+ 张瓦片。如果并发请求过多,浏览器会触发连接池限制(Chrome 默认 6 个同域并发),后面 50+ 个请求排队,首屏时间长达 8 秒。
解决方案:实现瓦片优先级队列。根据瓦片与视图中心的距离排序,距离越近优先级越高;同时限制并发数为 4:
const queue = new PriorityQueue(tiles.map(tile => ({ tile, priority: distanceToCenter(tile, center) }))); let activeCount = 0; function loadNext() { if (activeCount >= 4 || queue.isEmpty()) return; activeCount++; const item = queue.pop(); loadImage(item.tile.url).then(() => { activeCount--; loadNext(); }); }实测首屏时间从 8s 降到 1.2s。
5.3 移动端双指缩放的“撕裂感”:手指一动,底图和标记不同步
在 iOS Safari 上,georoam事件触发频率远低于渲染帧率。用户双指缩放时,graphic瓦片图层每帧重绘,但series的散点图因为convert计算耗时,会延迟 2-3 帧才更新,造成视觉撕裂。
解决方案:对convert函数做性能优化。将getMercatorViewRange的计算结果缓存,并用requestIdleCallback延迟非关键计算:
let cachedViewRange = null; echarts.registerCoordinateSystem('mercator', { create: function(ecModel, api) { return { convert: function(data) { // 缓存视图范围,避免每帧重复计算 if (!cachedViewRange) { cachedViewRange = getMercatorViewRange(api.getViewRect(), this); } return mercatorToPixel(data, api.getViewRect(), cachedViewRange); } }; } }); // 在 georoam 后,用 requestIdleCallback 更新缓存 chartInstance.on('georoam', () => { requestIdleCallback(() => { cachedViewRange = null; // 清空缓存,下次 convert 时重建 }); });5.4 天地图密钥的“静默失效”:凌晨三点,监控告警
天地图密钥有 24 小时有效期,且不主动通知。某次凌晨运维告警,发现所有地图白屏。排查发现是密钥过期,但前端没有任何错误提示——天地图服务返回的是 200 状态码的透明 PNG,ECharts 加载后显示为空白。
解决方案:在瓦片Image.onload中,用 canvas 检测图片是否为空白:
function checkImageBlank(img) { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); canvas.width = img.width; canvas.height = img.height; ctx.drawImage(img, 0, 0); const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data; // 检查所有像素的 alpha 通道是否为 0 for (let i = 3; i < data.length; i += 4) { if (data[i] > 0) return false; // 有非透明像素 } return true; } img.onload = () => { if (checkImageBlank(img)) { console.error('天地图瓦片加载失败:密钥可能已失效'); // 触发密钥刷新逻辑 } };5.5 ECharts 版本升级的“兼容性断崖”:从 5.4 到 5.5,坐标系全乱
ECharts 5.5 重构了geo组件的内部坐标处理流程,getViewRect()返回值结构变了,roam事件参数格式也变了。我们原来的mercator坐标系在 5.5 下完全失效,所有点都飞到左上角。
解决方案:建立版本适配层。在registerCoordinateSystem中检测 ECharts 版本:
const version = echarts.version.split('.').map(Number); if (version[0] === 5 && version[1] >= 5) { // 使用 5.5+ 的新 API api.getViewRect(); // 新返回结构 } else { // 使用旧 API api.getDom().getBoundingClientRect(); // 降级获取 }同时,所有项目锁定 ECharts 版本(如"echarts": "5.4.3"),升级前必须全链路回归测试。
6. 完整可运行示例:从零开始的最小可行代码
下面是一个去掉所有业务逻辑、仅保留核心功能的最小可运行示例。复制粘贴即可在本地 HTML 中运行(需替换your_api_key):
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>ECharts + 天地图最小示例</title> <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script> <style> #main { width: 100%; height: 600px; } </style> </head> <body> <div id="main"></div> <script> // 1. 注册 Web Mercator 坐标系 echarts.registerCoordinateSystem('mercator', { create: function(ecModel, api) { let cachedViewRange = null; function getMercatorViewRange(viewRect, zoom, center) { const resolution = 156543.03392 / Math.pow(2, zoom); const halfWidthM = (viewRect[2] / 2) * resolution; const halfHeightM = (viewRect[3] / 2) * resolution; // WGS84 to Web Mercator const lon = center[0], lat = center[1]; const x = lon * 20037508.34 / 180; const y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180); const centerY = y * 20037508.34 / 180; const centerX = x; return { minX: centerX - halfWidthM, maxX: centerX + halfWidthM, minY: centerY - halfHeightM, maxY: centerY + halfHeightM }; } return { convert: function(data) { if (!cachedViewRange) { const zoom = ecModel.getComponent('geo').option.zoom || 4; const center = ecModel.getComponent('geo').option.center || [104.0, 32.0]; const viewRect = api.getViewRect(); cachedViewRange = getMercatorViewRange(viewRect, zoom, center); } const { minX, maxX, minY, maxY } = cachedViewRange; const xPx = viewRect[0] + (data[0] - minX) / (maxX - minX) * viewRect[2]; const yPx = viewRect[1] + (data[1] - minY) / (maxY - minY) * viewRect[3]; return [xPx, yPx]; }, deConvert: function(px) { const viewRect = api.getViewRect(); const zoom = ecModel.getComponent('geo').option.zoom || 4; const center = ecModel.getComponent('geo').option.center || [104.0, 32.0]; const { minX, maxX, minY, maxY } = getMercatorViewRange(viewRect, zoom, center); const x = minX + (px[0] - viewRect[0]) / viewRect[2] * (maxX - minX); const y = minY + (px[1] - viewRect[1]) / viewRect[3] * (maxY - minY); return [x, y]; } }; } }); // 2. WGS84 转 Web Mercator 工具函数 function wgs84ToMercator([lng, lat]) { const x = lng * 20037508.34 / 180; const y = Math.log(Math.tan((90 + lat) * Math.PI / 360)) / (Math.PI / 180); const yM = y * 20037508.34 / 180; return [x, yM]; } // 3. 瓦片管理器(简化版) function getTilesInRect(minX, maxX, minY, maxY, zoom) { const n = Math.pow(2, zoom); const tileMinX = Math.max(0, Math.floor((minX + 20037508.34) / (40075016.68 / n))); const tileMaxX = Math.min(n - 1, Math.floor((maxX + 20037508.34) / (40075016.68 / n))); const tileMinY = Math.max(0, Math.floor((20037508.34 - maxY) / (40075016.68 / n))); const tileMaxY = Math.min(n - 1, Math.floor((20037508.34 - minY) / (40075016.68 / n))); const tiles = []; for (let x = tileMinX; x <= tileMaxX; x++) { for (let y = tileMinY; y <= tileMaxY; y++) { tiles.push({ x, y, z: zoom }); } } return tiles; } function drawTiles(tiles, viewRect, zoom, apiKey) { const graphicElements = []; const tileSizeM = 40075016.68 / Math.pow(2, zoom); tiles.forEach(tile => { const url = `https://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX=${zoom}&TILEROW=${tile.y}&TILECOL=${tile.x}&tk=${apiKey}`; const tileMinX = -20037508.34 + tile.x * tileSizeM; const tileMaxX = tileMinX + tileSizeM; const tileMaxY = 20037508.34 - tile.y * tileSizeM; const tileMinY = tileMaxY - tileSizeM; const [x1, y1] = [viewRect[0], viewRect[1]]; const [x2, y2] = [viewRect[0] + viewRect[2], viewRect[1] + viewRect[3]]; // 简化:假设瓦片铺满整个视图(实际需精确计算) graphicElements.push({ type: 'image', style: { image: url, x: viewRect[0], y: viewRect[1], width: viewRect[2], height: viewRect[3] } }); }); return graphicElements; } // 4. 初始化图表 const chartDom = document.getElementById('main'); const myChart = echarts.init(chartDom); // 5. 配置选项 const option <p> <a href="https://download.csdn.net/download/m0_37631110/87357540" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>