天地图API 4.0+Geojson:省市级行政区色块专题图制作全攻略

发布时间:2026/9/17 4:18:31
天地图API 4.0+Geojson:省市级行政区色块专题图制作全攻略
做地图专题图的朋友大概率都接过这样的需求把某个省的所有地级市用不同颜色块铺满再把每个城市名字标上去一眼就能看出全省的分布格局。用天地图API 4.0来实现说简单也简单说坑也是真的多。我第一次提交时色块和底图整整偏了几公里边界对不上、文字乱飞排查了大半天才发现是坐标系在作祟。这篇文章就把“天地图API 4.0 geojson 全省市级色块 文字标记”这条链路完整拆开来讲从数据准备、代码实现到性能优化和常见问题一次性说透。不管你是做政府项目、农业普查、人口统计还是单纯的兴趣项目只要想在天地图上做省市级的专题色块图这篇文章都能直接当参考手册用。文中代码以四川省21个市州为例但逻辑完全通用换成任何一个省都只需要换数据和中心点坐标。1. 内容整体设计与思路拆解1.1 一句话说清这个需求要解决什么需求拆开来看其实是三件事把天地图底图加载出来把省市级行政区边界用geojson数据渲染成色块再把城市名称标注在色块中心。单独任何一件都不难难的是三件事拼在一起时各种细节容易翻车。先说说为什么需要geojson这个中间层。天地图API本身只是提供底图瓦片和基础覆盖物能力它并不知道“四川省有21个市州”这件事。你必须自己准备一份描述边界的数据告诉地图渲染引擎“这个多边形是成都那个多边形是绵阳”。geojson就是这个通用交换格式它用FeatureCollection来组织多个行政区每个Feature的geometry字段存坐标环properties字段里塞区域名称、编码、统计数据等属性。我见过很多新手直接跳过数据检查拿一份网上下载的geojson就往页面上怼结果要么边界整体偏移、要么渲染出来一团乱麻。所以整个项目的第一设计决策不是写代码而是先保证数据靠谱。1.2 方案选型官方API原生实现还是叠加第三方库天地图API 4.0提供了覆盖物体系包括多边形和文字标注。用它来实现色块和文字本质上是一个“数据格式转换”的过程把geojson的Geometry结构转成TMap的MultiPolygon和MultiLabel能识别的结构。但实际操作中业内还有另一条路把天地图瓦片当作底图再用Leaflet或OpenLayers做数据图层叠加很多教程也这么教。这个方案的优势是可视化生态丰富样式定制灵活适合复杂交互的项目。缺点是额外引一个框架二开成本和对官方API的熟悉度要求都不一样。我的建议是如果需求就是“全省色块城市名”老老实实用天地图API 4.0的原生类就够了。MultiPolygon本身就是为批量化多边形渲染设计的性能比逐个new Polygon再addTo好一个数量级MultiLabel同理。直接用官方API少引依赖、少踩框架版本冲突的坑以后升级维护也方便。如果后期需求膨胀到需要复杂弹窗、数据联动、图表分析再考虑引入其他库也不迟。1.3 核心流程底图 → 数据解析 → 色块 → 文字整个实现的流程用文字描述是这样的在页面初始化天地图底图设置中心点和缩放级别。通过fetch或axios异步加载本地的geojson文件。遍历FeatureCollection把每个feature的geometry坐标提取出来转换成TMap.MultiPolygon需要的paths数组格式。为每个城市分配一个颜色样式生成色块图层并add到地图上。遍历所有城市计算每个多边形的地理中心点坐标生成TMap.MultiLabel文字标注。配合地图事件或扩展需求加上悬浮提示、点击高亮、分级配色等。这个流程看起来直白但每一步都有隐藏的雷区。比如geometry包含Polygon和MultiPolygon两种类型转换时处理不好就会少画几个城市再比如质心计算如果简单取坐标平均值遇到凹多边形时文字会飘到区域外面去。这些细节会在后面逐一展开。2. 核心细节解析与实操要点2.1 geojson数据从哪来拿到之后先做什么市界数据来源很多。常见的是从阿里DataV的Atlas平台下载全国省市区划geojson也有各种开源仓库整理了带统计字段的行政边界数据还有人从项目里导出过离线数据。不管来源是什么拿到数据后必须先做三件事确认坐标系、检查属性字段、评估数据量。坐标系是第一个大坑。天地图官方底图采用的是CGCS2000坐标系国内各种数据来源却五花八门有的是WGS84经纬度有的是GCJ02火星坐标甚至有把投影平面坐标直接存成经纬度的。GCJ02坐标如果直接渲染到天地图上会出现肉眼可见的偏移大概几百米到一公里在市级边界上尤其明显。WGS84和CGCS2000在当前精度下差异很小一般可以直接用。所以拿到数据后先看属性字段里有没有coordinateSystem或epsg标记没标记就手工抽一条边界坐标去百度地图或高德地图上比对一下确认不偏再往下走。属性字段的检查也很重要。geojson的properties里有没有name字段、adcode字段决定了你后面怎么取城市名、怎么绑定统计数据。有些数据源字段名是中文有些是英文缩写在写代码前先console.log打印一条feature看清楚能省很多调试时间。数据量方面一个省级的市级边界geojson压缩后通常几十到几百KB对前端来说压力不大。如果是全国县级边界几千个多边形就要考虑性能优化了后面专门讲。2.2 TMap.MultiPolygon的坐标结构从geojson到paths天地图API 4.0的MultiPolygon接收的geometries数组里每个元素包含paths字段。paths的结构是一个二维数组最外层是多边形的多个环外环和洞每个环由经纬度点数组构成每个点的格式是[lng, lat]。geojson里Polygon类型的数据结构是{ type: Polygon, coordinates: [ [[lng, lat], [lng, lat], ...], [[lng, lat], [lng, lat], ...] ] }第一个数组是外环后面的数组是内环如果有洞的话。对照可以看出从geojson到TMap的paths基本可以直接映射Polygon的coordinates就是TMap需要的paths数组。而geojson的MultiPolygon类型外层有一个polygon数组每个polygon又包含多个ring转换时要先展开。我封装了一个转换函数兼顾两种类型function geojsonToTmpaths(geometry) { if (!geometry) return []; if (geometry.type Polygon) { return geometry.coordinates; } if (geometry.type MultiPolygon) { let result []; geometry.coordinates.forEach(polygon { polygon.forEach(ring { result.push(ring); }); }); return result; } return []; }注意一个容易忽略的细节有些geojson数据的坐标环首尾没有闭合也就是第一个点和最后一个点不同。TMap渲染时对闭合性要求没那么严格但为了保险在转换时最好做一次闭合检查。如果发现首尾不闭合手动把第一个点push到底部避免出现肉眼可见的缺口。2.3 文字标注的核心质心计算和Label样式城市名要放在色块中间就得先算出每个多边形的中心点。最简单粗暴的办法是坐标平均法把边界所有点经纬度加起来求平均。这个方法对凸多边形效果好但对凹多边形、L形或不规则形状算出来的中心点很可能落在区域外面文字就飘到旁边城市去了。我做省级市界图时发现部分城市边界包含较多岛屿或复杂弯曲平均法容易出问题。更稳的是多边形面积加权质心算法把多边形分解成多个三角形按面积加权算重心。虽然计算量比平均法大但对于前端一帧计算几十个多边形完全没压力。一个简化版实现function calcPolygonCenter(coords) { let area 0; let cx 0; let cy 0; for (let i 0; i coords.length - 1; i) { let xi coords[i][0]; let yi coords[i][1]; let xj coords[i 1][0]; let yj coords[i 1][1]; let temp xi * yj - xj * yi; area temp; cx (xi xj) * temp; cy (yi yj) * temp; } area area / 2; cx cx / (6 * area); cy cy / (6 * area); return [cx, cy]; }如果geometry是MultiPolygon就取面积最大的那个polygon计算质心而不是简单取第一个polygon因为有些城市的主城区之外还有离岛取错子区域文字会标到海上去。标签样式的设置同样有讲究。天地图的LabelStyle支持设置颜色、字号、对齐方式和背景色。我把城市名的样式统一设计为深色文字、半透明白底这样无论底下色块是深是浅文字都清晰可读。padding给到4到6像素避免文字贴边。3. 实操过程与核心环节实现3.1 第一步注册天地图应用并配置tk密钥天地图API 4.0要求所有请求携带tk密钥。前往天地图官网注册账号在控制台创建一个浏览器端应用添加域名白名单。本地调试时白名单里要有http://localhost和http://127.0.0.1上线后换成正式域名。这一步最容易踩的坑是密钥类型选错。天地图有浏览器端和服务器端两类密钥浏览器端tky是可以直接暴露在前端页面里的服务器端sk不能出现在浏览器代码中。做前端色块渲染请务必创建浏览器端应用并复制tk。页面引入官方JS库script srchttps://api.tianditu.gov.cn/api?v4.0tkYOUR_TIANDITU_KEY typetext/javascript/script引入之后全局会注册TMap命名空间所有天地图4.0的类都从这里取。3.2 第二步初始化地图底图初始化代码非常简单但有几个参数要解释一下。center的取值是整个项目能否一上来就看到完整效果的关键。以四川省为例省会成都在东经104.06度、北纬30.67度附近全省范围东西跨度约1000公里zoom设置为6屏幕上就能显示出完整省界和主要城市的轮廓。let map new TMap.Map(map, { center: [104.06, 30.67], zoom: 6, viewMode: 2D, baseMap: { type: vec } });baseMap.type支持vec矢量、img影像、ter地形等。做行政色块专题图一般用vec矢量底图色彩干净、边界清晰和自定义色块叠加后不会被影像图上的纹理干扰。如果你要做的是遥感信息展示也可以换成img影像模式。3.3 第三步加载并解析geojson数据推荐把市界geojson文件放在与页面同级的data目录下用fetch异步加载。为什么不建议直接写在JS文件里因为一个省的数据至少包含几千个坐标点直接写死在代码里既影响可读性也不方便后续替换成全国数据。fetch(./data/sichuan.json) .then(res res.json()) .then(data { renderProvince(data); }) .catch(err { console.error(geojson加载失败, err); });这里必须提醒用fetch加载本地json文件时如果直接用file://协议打开页面浏览器会拦截跨域请求导致数据加载失败。正确做法是在本地起一个静态服务器用VS Code的Live Server插件或者全局安装http-server启动后在localhost端口访问页面。拿到data之后先打印一条feature看看属性字段console.log(data.features[0]);确认properties里有name字段再继续往下写。3.4 第四步生成色块图层色块的核心是TMap.MultiPolygon。我先把每个城市对应的样式预定义好再组装geometries数组一次性批量渲染性能碾压循环add Polygon的老写法。配色方案上和专题图的业务含义强相关。如果是做分类统计每个类别用同一色系深浅区分如果只是纯粹让相邻城市颜色不同就从色环中隔几个选一个基础色。我习惯用10到15种柔和色循环分配避免相邻城市大面积撞色。let styleColors [ #ffa07a, #87ceeb, #98fb98, #ffd700, #dda0dd, #f08080, #20b2aa, #ffb6c1, #b0e0e6, #ffdab9 ]; let styles {}; data.features.forEach((feature, index) { let styleId city_ index; styles[styleId] new TMap.PolygonStyle({ color: styleColors[index % styleColors.length], showBorder: true, borderColor: #ffffff, borderWidth: 1 }); }); let geometries []; data.features.forEach((feature, index) { let paths geojsonToTmpaths(feature.geometry); geometries.push({ paths: paths, styleId: city_ index }); }); let multiPolygon new TMap.MultiPolygon({ map: map, styles: styles, geometries: geometries });这里有个提升效果的小技巧borderColor统一用白色borderWidth给1到2像素既能把相邻城市清晰区隔开又不会在视觉上喧宾夺主。如果项目要求色块边缘更柔和可以给color加透明度比如rgba格式或者用color的alpha参数。3.5 第五步生成城市文字标注文字标注用TMap.MultiLabel。和MultiPolygon一样MultiLabel也是批量类接收一个geometries数组每个元素包含坐标点、styleId和content文本信息。let labelGeometries data.features.map((feature, index) { let center calcLabelCenter(feature); return { lng: center[0], lat: center[1], styleId: cityLabel, content: feature.properties.name }; }); let multiLabel new TMap.MultiLabel({ map: map, styles: { cityLabel: new TMap.LabelStyle({ color: #333333, size: 14, align: center, verticalAlign: middle, backgroundColor: rgba(255, 255, 255, 0.75), padding: new TMap.Point(4, 4) }) }, geometries: labelGeometries });calcLabelCenter函数里我做了类型判断和质心算法选择。对Polygon直接取外环计算面积加权质心对MultiPolygon先找出面积最大的子多边形再计算质心。Word提醒一点有些geojson的地级市面里包含了大量岛屿环内环在质心计算时必须忽略否则会把质心拉到奇怪的位置。3.6 完整演示代码四川省21市州色块标注把上面各部分拼起来一个可以直接运行的页面是这样的!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title天地图API 4.0 省市色块标注/title script srchttps://api.tianditu.gov.cn/api?v4.0tkYOUR_TIANDITU_KEY typetext/javascript/script style html, body, #map { margin: 0; padding: 0; width: 100%; height: 100vh; } /style /head body div idmap/div script let map new TMap.Map(map, { center: [104.06, 30.67], zoom: 6, viewMode: 2D, baseMap: { type: vec } }); function geojsonToTmpaths(geometry) { if (!geometry) return []; if (geometry.type Polygon) { return geometry.coordinates; } if (geometry.type MultiPolygon) { let result []; geometry.coordinates.forEach(polygon { polygon.forEach(ring { result.push(ring); }); }); return result; } return []; } function calcPolygonAreaAndCenter(coords) { let area 0; let cx 0; let cy 0; for (let i 0; i coords.length - 1; i) { let xi coords[i][0]; let yi coords[i][1]; let xj coords[i 1][0]; let yj coords[i 1][1]; let temp xi * yj - xj * yi; area temp; cx (xi xj) * temp; cy (yi yj) * temp; } area area / 2; if (Math.abs(area) 0.000001) { return null; } cx cx / (6 * area); cy cy / (6 * area); return [cx, cy]; } function calcLabelCenter(feature) { let geometry feature.geometry; if (geometry.type Polygon) { let center calcPolygonAreaAndCenter(geometry.coordinates[0]); if (center) return center; return geometry.coordinates[0][0]; } if (geometry.type MultiPolygon) { let maxArea -Infinity; let bestCenter null; geometry.coordinates.forEach(polygon { let center calcPolygonAreaAndCenter(polygon[0]); if (!center) return; let area Math.abs(polygon[0].reduce((sum, p, i, arr) { let next arr[(i 1) % arr.length]; return sum (p[0] * next[1] - next[0] * p[1]); }, 0) / 2); if (area maxArea) { maxArea area; bestCenter center; } }); if (bestCenter) return bestCenter; return geometry.coordinates[0][0][0]; } return [0, 0]; } fetch(./data/sichuan.json) .then(res res.json()) .then(data { let styleColors [ #ffa07a, #87ceeb, #98fb98, #ffd700, #dda0dd, #f08080, #20b2aa, #ffb6c1, #b0e0e6, #ffdab9 ]; let styles {}; let geometries []; data.features.forEach((feature, index) { let styleId city_ index; styles[styleId] new TMap.PolygonStyle({ color: styleColors[index % styleColors.length], showBorder: true, borderColor: #ffffff, borderWidth: 1 }); geometries.push({ paths: geojsonToTmpaths(feature.geometry), styleId: styleId }); }); new TMap.MultiPolygon({ map: map, styles: styles, geometries: geometries }); let labelGeometries data.features.map((feature, index) { let center calcLabelCenter(feature); return { lng: center[0], lat: center[1], styleId: cityLabel, content: feature.properties.name }; }); new TMap.MultiLabel({ map: map, styles: { cityLabel: new TMap.LabelStyle({ color: #333333, size: 14, align: center, verticalAlign: middle, backgroundColor: rgba(255, 255, 255, 0.75), padding: new TMap.Point(4, 4) }) }, geometries: labelGeometries }); map.setCenter([104.06, 30.67]); map.setZoom(6); }) .catch(err { console.error(geojson加载失败, err); }); /script /body /html提示以上代码中的YOUR_TIANDITU_KEY需替换为你自己在天地图官网申请到的浏览器端tk密钥。4. 常见问题与排查技巧实录4.1 问题速查表把我在实际开发和工区同事反馈中遇到的高频问题整理成了一张表遇到问题可以直接对号入座现象可能原因解决办法页面空白地图不显示tk密钥错误、域名白名单没配检查tk是否正确确认白名单包含当前域名和端口色块整体偏移边界对不上底图geojson坐标系与底图不一致确认数据是CGCS2000/WGS84若是GCJ02需先纠偏geojson加载失败报CORS错误file://直接打开或跨域未配置本地起静态服务器用localhost访问或给服务设置CORS头渲染后少了一些城市多边形geometry类型判断遗漏确认转换函数同时处理Polygon和MultiPolygon所有色块一个颜色styleId没正确关联检查geometries里每个元素是否都指定了存在的styleId城市名飘到区域外面质心计算用了简单平均法换成面积加权质心算法或手工微调label位置地图卡顿、拖动掉帧多边形数量大或坐标点过多简化边界数据关闭多余交互必要时实现分帧渲染标签文字模糊字号设置太小或缩放级别低适当调大字号或给label加背景色提高对比度这张表每一行都是真实场景验证过的尤其是坐标偏移和跨域问题是新手翻车比例最高的两个点。4.2 性能优化从全省到全国数据量的应对策略如果需求从省级扩展到全国341个地级以上的多边形同时渲染再加上县级数据事情就变得复杂了。多边形数量上去了除了MultiPolygon批量渲染之外还需要考虑三个层面的优化。第一是边界数据简化。geojson里每个边界点如果精度过高坐标点数会非常多。可以使用地图简化算法比如Douglas-Peucker算法在保证轮廓基本不变的前提下将坐标点数压缩到原来的1/3甚至更少。网上有简化库可以直接用比如Simplify.js压缩后对视觉影响很小。第二是分帧渲染。如果数据量实在太大一次性把几千个多边形全部交给MultiPolygon地图初始化时会卡顿一两秒。可以把geometries数组切分成小组每帧往MultiPolygon里追加一组MultiPolygon有setGeometries方法配合requestAnimationFrame控制节奏让渲染过程平滑进行。第三是视口裁剪和按需加载。对全国范围的数据可以先根据比例尺判断需要展示的级别低缩放级别时只渲染省级面放大到市级级别再加载市级边界再放大才加载县级边界。这样资源消耗大幅下降交互顺畅度提升明显。文字标注的优化同理。市名标注数量通常不大但如果是县级标注几百上千个label也会有性能压力。可以设置minZoom在低缩放级别下隐藏一部分小城市名放大后再显示既保证可读性又保证性能。4.3 交互增强点击高亮和悬浮提示基础色块图做完很多业务场景还要加交互。我常用的增强功能有四个点击城市高亮、悬浮显示统计信息、点击弹出详请面板、按指标分级配色。点击高亮的实现思路是在MultiPolygon上绑定点击事件拿到当前feature后把该feature的styleId临时换成高亮样式同时把其他所有feature恢复默认样式。注意不要每次点击都重建MultiPolygon直接调用setStyles或setGeometries局部更新即可。分级配色是专题图的灵魂。核心逻辑是先从统计数据里算出最大值最小值按区间把城市分成几级每一级映射到同一个色系的深浅颜色。比如深色表示高值、浅色表示低值图例再标注对应的数值区间。这个思路在人口密度、生产总值、资源分布等场景下非常实用。实现时只需要改styles映射的生成逻辑几何结构完全不用动。悬浮提示可以使用TMap的Popup或InfoWindow在mouseover时显示城市名称和对应指标数据。但要注意频繁触发弹窗会遮挡地图拖拽操作建议用延迟显示和延迟关闭来控制交互体验。4.4 关于坐标转换的避坑补充前面强调了坐标系风险这里再说细致一些。如果你的geojson数据来自高德生态大概率是GCJ02火星坐标直接渲染到天地图上会有明显偏移。这时需要做一次坐标纠偏通用的做法是引入coordtransform库将每个坐标点从GCJ02转换到WGS84。虽然WGS84和CGCS2000仍有细微差异但在这个场景下已经足够准确。另一个容易忽视的问题是有些数据源的经纬度顺序是反的即lat在前、lng在后。如果渲染出来的地图点都跑到海里或者堆在一起优先检查数据里第一个数字到底是经度还是纬度。天地图API的坐标点统一使用[lng, lat]顺序和geojson规范一致但很多人从Excel或CSV里手拼数据时容易搞反这类问题给我发来的求助里占比不低。5. 从专题图到数据可视化的几点延伸建议色块加文字只是第一步真正让地图产生价值的是数据表达。做完基础展示后我一般会往下延伸两层。第一层是让色块“带数据”。给每个城市的properties里塞入指标字段比如人口、GDP、病例数、降水量然后用分级配色方案渲染让颜色深浅直接反映数值大小。这样地图就不再是单纯的行政区划图而是一张可读性很强的统计专题图。第二层是让地图“能联动”。点击某个城市旁边图表联动展示时序趋势或者反过来点击统计图表中的城市名地图自动飞到对应区域并高亮。这种联动效果在业务汇报里特别加分。开发时可以用乘法器的思路地图事件抛出城市编码图表组件监听并更新状态管理统一维护选中城市。再提一个易用性细节加上图例和比例尺。天地图4.0提供了TMap.Control类可以直接添加比例尺控件。图例则需要自己用HTML实现放在地图的角落色块颜色和数值区间对应清楚。别看图例是小部件业务方看专题图时第一眼找的就是图例在哪里。针对行政区边界数据更新的问题也想多说一句。行政区划会调整比如县级市改区、撤地设市如果项目长期运营建议定时更新geojson数据源并在代码里做好版本号或更新时间标记避免你拿着旧边界图给业务方汇报时闹尴尬。在实际项目里我还有一个习惯geojson文件单独建一个data目录文件名带上数据日期比如sichuan_2025_v2.json。这样每次更新后git历史里能清楚地看到底图数据的变化排查问题也方便回滚。这个技能熟练之后往全国、往县级扩展都只是数据替换的问题。你把省级代码跑通一次后面的路就顺畅多了。