☰
GeoJSON世界地图数据包解析与可视化下钻实践
2026/10/26 9:14:46 网站建设 项目流程

简介:面向地图可视化开发者与前端数据工作者的世界地图及主要国家 GeoJSON 数据合集,适合用于全球视角的地图展示、国家区域钻取、边界高亮与空间数据预处理等场景。压缩包共 25 个文件,其中 23 个为 JSON 格式,提供世界及多个主要国家的边界几何数据,另有 2 个 MHT 网页存档,可作为 D3.js 地图相关内容的学习参考;整体约 11.31MB。已有 6266 人浏览或学习,具备一定的使用验证基础。数据以标准 GeoJSON 组织,便于在 D3.js 等可视化方案中直接读取,也可作为地理围栏、区域统计与教学演示的基础素材。由于原始边界文件体积较大,使用者可结合项目精度要求进行简化,以平衡渲染性能与地图细节,从而快速搭建世界地图和国家层级的地图应用原型。

1. 一张世界地图,改了七版才算完

做数据可视化那阵子,产品给的需求是:首页铺一张世界地图,主要国家按业务指标染色,点某个国家进去看该国的下级明细。设计师先给了一版约 2MB 的 SVG,打开一看每个国家的路径都是手工铺的点,点击取不到国家名,改配色要在上千行的 path 里翻。第二次换成后端画好的静态 PNG,屏幕一缩放就糊。第三次开始,才认真整理手头这份「世界地图 + 主要国家」的 JSON(GeoJSON)数据包,把它当成数据来管,而不是当成图片来用。它本质上就是一组标准 GeoJSON 文件:一份世界总图,加上按国家切好的边界文件,每个要素都带着名称、ISO 编码和几何多边形。适合三类人:做数据大屏和地图下钻的前端、要按国家维度做统计后端、以及准备自己搭一套地图数据流水线的人。下面按我当时的落地顺序拆:结构怎么读、怎么跑起来、怎么改。

2. 文件结构拆解:世界总图与分国家边界是怎么组织的

拿到一个地图数据包,第一反应不该是打开渲染,而是先搞清楚「这一坨 JSON 里到底有什么」。GeoJSON 是标准格式,但不同来源的数据在字段命名、坐标精度、几何类型上差别很大,先看清结构,后面能省掉大量返工。

2.1 先把 FeatureCollection 这层壳拆开

打开世界总图文件,第一行看到的是"type": "FeatureCollection",这不是随便写的,它规定了这个文件只能装要素的集合。每个要素是一个国家或地区,形状信息在geometry里,属性信息在properties里。很多人渲染失败,就是只看geometry不看properties,结果满地图画出来了却没有一个能绑数据。

先写个几十行的脚本,把壳子里有什么打出来,比在编辑器里肉眼翻强得多:

// inspect.js —— 读取世界总图,只做结构勘察,不做渲染 const fs = require('fs'); const raw = fs.readFileSync('./data/world.geo.json', 'utf8'); const world = JSON.parse(raw); console.log('顶层类型:', world.type); // 期望 'FeatureCollection' console.log('要素数量:', world.features.length); // 国家/地区条目数,不是文件数 console.log('第一个要素的键:', Object.keys(world.features[0])); console.log('第一个要素的属性:', JSON.stringify(world.features[0].properties, null, 2));

这段代码的价值不在输出结果,在于它把「你手里的数据长什么样」变成可复现的事实。features.length一般不等于全世界国家数,因为有些数据源会把海外领地、争议区域、无人岛单独拆成要素,也会把同一个国家拆成好几块。properties里通常能找到几个关键字段:name(显示名,可能是中文也可能是英文)、name_en、name_zh、iso_a2(两位国家码)、iso_a3(三位国家码)、continent或region(大洲/区域)。这四个字段里,只有 ISO 码是稳定的,名称字段各家数据源写法不一样,有的是全称有的是简称,拿name当联合主键,早晚翻车。

接着看几何类型的分布,这直接决定后面的渲染成本和代码分支:

// 统计每个要素的几何类型,Polygon 和 MultiPolygon 的比例决定渲染成本 const geoStat = world.features.reduce((acc, f) => { const t = f.geometry ? f.geometry.type : 'null'; acc[t] = (acc[t] || 0) + 1; return acc; }, {}); console.log('几何类型分布:', geoStat); // 典型输出: { Polygon: 180, MultiPolygon: 70, null: 2 }

Polygon是单块陆地,MultiPolygon是碎成好几块的国家(岛屿、飞地)。null的要素通常是只有属性没有边界的占位条目,渲染时必须过滤掉,否则你的绘图函数会在coordinates上读到undefined直接抛异常。另外统计一下坐标点总数,这是判断数据精度的最直观指标:

// 递归数出所有坐标点,注意 Polygon 和 MultiPolygon 的嵌套层数不同 function countPoints(geometry) { if (!geometry) return 0; const walk = (arr) => Array.isArray(arr[0]) ? arr.reduce((sum, sub) => sum + walk(sub), 0) : 1; return walk(geometry.coordinates); } const totalPoints = world.features.reduce((sum, f) => sum + countPoints(f.geometry), 0); console.log('坐标点总数:', totalPoints, '文件大小(KB):', (raw.length / 1024).toFixed(1));

countPoints用了一个取巧判断:如果coordinates[0]还是数组,说明没到底,继续递归;到底了就是一个[lng, lat]点,记 1。这样同一份代码能同时处理两种几何类型,不用写两套分支。跑完这两个统计,你基本就能判断:这份数据是给大屏粗看用的(几万点),还是给 GIS 精细分析用的(几十万点以上)。

2.2 世界图与分国家文件的对应关系

这份数据包的组织方式通常是「一总多分」:根目录一份世界总图,一个countries/子目录放分国家文件,文件名用 ISO 码命名,比如CN.geo.json、US.geo.json。这种约定的好处是世界图和分国家文件天然能对上——点世界图上的某个国家,拿它的 ISO 码拼出文件路径,直接就能取到该国的细分数据,不需要维护一张额外的映射表。

路径内容典型用途
world.geo.json世界总图,全部国家要素首屏底图、全局染色
countries/<ISO>.geo.json单个国家的边界(含下级行政区)点击下钻、单国详情页
meta.json国家码到显示名的映射、中心点坐标图例、标签定位

有一件事必须在项目一开始就定死:分国家文件用两位码还是三位码。两位码(CN、US)更短,但和某些业务系统里的三位码(CHN、USA)对不上;三位码更规范,但路径长一点。选哪个都行,关键是全库统一,别一半两位一半三位。我见过一份数据包,欧洲国家用三位码、亚洲国家用两位码,写出来的路径拼接函数要多写一层 try/catch 兜底。

meta.json属于常见做法里的加分项:把国家码对应的中文名、英文名、地图上的标注经纬度单独抽出来。抽出来的理由很实在——渲染时你只关心几何,但图例、tooltip、标签全都需要名字和中心点,混在要素属性里会让每个要素的体积变大,而且改文案要动整个地图文件。

2.3 精度决定体积:为什么你的 20MB 加载要 8 秒

地图数据最容易忽略的成本是体积。同样轮廓,坐标保留 6 位小数和保留 2 位小数,文件大小能差十倍以上。地理坐标的换算关系是这样的:

小数位单点精度适用场景
6 位约 0.11 米精细 GIS、城市级放大
4 位约 11 米国家级地图足够
3 位约 110 米大屏、缩略图
2 位约 1.1 公里极小体积的轮廓示意

0.000001度在赤道附近大约是 0.11 米,这个精度对一张要在浏览器里画全世界的地图来说完全是浪费——屏幕上一个像素在全世界缩放级别下代表的距离是几十公里。所以拿到原始数据后,第一件该做的事是截断小数位,而不是直接丢给前端。

截断的代码看着简单,坑在嵌套层级上,每个类型都不一样:

// 递归截断小数位,兼容 Polygon 和 MultiPolygon 两种嵌套 function truncateCoords(coords, digits) { const factor = Math.pow(10, digits); if (typeof coords[0] === 'number') { // 走到最底层的 [lng, lat] return [Math.round(coords[0] * factor) / factor, Math.round(coords[1] * factor) / factor]; } return coords.map(sub => truncateCoords(sub, digits)); } function truncateFeature(feature, digits) { if (!feature.geometry) return feature; return { ...feature, geometry: { ...feature.geometry, coordinates: truncateCoords(feature.geometry.coordinates, digits) } }; }

truncateCoords用的是递归下探:typeof coords[0] === 'number'就是到底了,是一个坐标点;否则继续 map 下去。这样Polygon(三层:环 → 点 → 数值)和MultiPolygon(四层:多边形 → 环 → 点 → 数值)都能过,不用判断类型。参数digits我一般取值 3 或 4,压测下来 4 位在 2K 屏幕上肉眼看不出差别,但体积能砍掉将近一半。

提示:截断会带来轻微的轮廓重叠或缝隙,尤其是相邻国家共享边界时,两个国家各自截断后可能在边界处错开几百米。大屏场景无所谓,但如果要按国界做面积统计或者做裁剪分析,保留原始精度,只用一份精简版做渲染、一份精确版做计算。

3. 跑起来:三步把世界地图画到页面上

数据看清楚了,接下来要让它出现在屏幕上。这一步最容易出的问题不是绘制算法,而是环境和坐标系——很多人第一次加载 GeoJSON 出现一片空白或者一团黑,八成是这两处出了岔子。

3.1 起一个本地服务再谈加载

如果你的index.html是直接双击用浏览器打开的,地址栏是file://开头,那么fetch('data/world.geo.json')一定失败,控制台会报跨域或者协议不支持。这不是代码问题,是浏览器的安全策略不允许file://下发起网络请求。所以第一步永远是起本地服务:

# 方式一:Python 自带的静态服务,几乎每台机器都有 python3 -m http.server 8000 # 方式二:Node 环境,不想装依赖就用 npx 临时拉 npx serve ./public -l 8000

目录结构要和服务根目录对齐,public/下放index.html和data/,这样页面里写/data/world.geo.json就能直接取到。加载代码本身很短,但两个细节值得说:

// 加载并缓存:地图数据一个页面只取一次,别在交互回调里重复请求 let worldCache = null; async function loadWorld() { if (worldCache) return worldCache; const res = await fetch('/data/world.geo.json'); if (!res.ok) throw new Error(`地图数据加载失败: ${res.status}`); worldCache = await res.json(); return worldCache; }

if (worldCache)这个短路很关键。地图文件动辄几 MB,如果每次点击、每次窗口 resize 都重新fetch一次,用户会看到控制台里一排相同的请求,弱网环境下直接卡死。res.ok判断也不能省——服务路径写错时fetch不会抛异常,它返回一个 404 的响应对象,你不判断就会在res.json()那一行抛出一个看不出原因的解析错误。

3.2 手写墨卡托投影,把经纬度变成屏幕坐标

GeoJSON 里的坐标是经纬度,单位是度,而你要画的是像素,中间必须做一次投影。地球是球面,摊到平面上一定会变形,常见的有等距圆柱投影(简单,高纬度拉伸严重)和墨卡托投影(保角,高纬度面积放大但是形状不变形,几乎所有在线地图都用它)。这里给一份不依赖任何地图库的实现,看得懂就能改:

// 墨卡托投影:把 [lng, lat] 映射到 SVG 画布的 [x, y] const W = 1000, H = 500; function project([lng, lat]) { // 经度线性映射:-180~180 度 → 0~W const x = (lng + 180) / 360 * W; // 纬度必须钳制,墨卡托在两极发散,超过 ±85.05 度 y 会变成 Infinity const clamped = Math.max(-85.05, Math.min(85.05, lat)); const rad = clamped * Math.PI / 180; const y = (0.5 - Math.log(Math.tan(Math.PI / 4 + rad / 2)) / (2 * Math.PI)) * H; return [x, y]; }

三个参数要理解清楚再改。W和H决定画布尺寸,墨卡托的宽高比理论上是 2:1,改成1200 × 600或1600 × 800都不会变形,但如果你设成1000 × 700,整个世界会被纵向拉伸。85.05是墨卡托投影的理论上限纬度,到这个值时y正好等于画布上下边界,超过就会得到无穷大,SVG 里拿到Infinity的路径会直接不渲染,表现出来就是「高纬度国家消失了」,这个现象我第一次遇到时查了半天数据,最后发现是投影公式的问题。Math.log(Math.tan(...))这半段是墨卡托的标准公式,不用改,记住它输出的y是「从上往下增大」的屏幕坐标方向就行。

有了投影函数,把多边形转成 SVG 路径就是字符串拼接:

// 环 → 路径字符串,第一个点用 M 起笔,后续用 L 连线,末尾 Z 闭合 function ringToPath(ring) { return ring.map((point, i) => { const [x, y] = project(point); return `${i === 0 ? 'M' : 'L'}${x.toFixed(2)},${y.toFixed(2)}`; }).join(' ') + ' Z'; } // 要素 → 路径,Polygon 和 MultiPolygon 统一成「多边形数组」处理 function featureToPath(feature) { const g = feature.geometry; if (!g) return ''; const polygons = g.type === 'Polygon' ? [g.coordinates] : g.coordinates; // 每个多边形可能有多条环,第一条是外轮廓,后面的是内部空洞 return polygons.map(poly => poly.map(ringToPath).join(' ')).join(' '); }

toFixed(2)是像素级精度,路径已经投影过了,两位小数足够,多余的位数只会让 DOM 变大。' Z'是 SVG 的闭合指令,GeoJSON 规范要求环的首尾点相同,但不同数据源未必严格遵守,加上Z能兜住这种情况。polygons.map(poly => poly.map(ringToPath))这层双重 map 保住了环的层级,如果直接拍平,内部空洞(湖泊、被包围的飞地)会被外轮廓覆盖,表现出来就是地图上多了几个实心色块。

最后把这些路径塞进 SVG:

// 渲染总图,data-key 存 ISO 码,供后续交互取用 function renderWorld(world, colorMap) { const paths = world.features .filter(f => f.geometry) // 过滤掉只有属性的占位要素 .map(f => { const key = f.properties.iso_a2 || f.properties.iso_a3; const fill = colorMap[key] || '#dfe6ef'; // 没数据的国家给中性灰,不要用白色 return `<path d="${featureToPath(f)}">// 计算要素集合的经纬度包围盒,用于下钻时重新适配视口 function getBBox(featureCollection) { let minLng = 180, maxLng = -180, minLat = 90, maxLat = -90; const walk = (arr) => { if (typeof arr[0] === 'number') { minLng = Math.min(minLng, arr[0]); maxLng = Math.max(maxLng, arr[0]); minLat = Math.min(minLat, arr[1]); maxLat = Math.max(maxLat, arr[1]); return; } arr.forEach(walk); }; featureCollection.features.forEach(f => f.geometry && walk(f.geometry.coordinates)); return { minLng, maxLng, minLat, maxLat }; } // 点击下钻:取>// 距离阈值抽稀:相邻点距离小于 minDistDeg 度的丢弃 function simplifyRing(ring, minDistDeg) { if (!ring || ring.length < 4) return ring; const out = [ring[0]]; for (let i = 1; i < ring.length; i++) { const [lng, lat] = ring[i]; const [prevLng, prevLat] = out[out.length - 1]; // 用经纬度差值近似距离,低纬度误差小,高纬度偏大,做视觉精简够用 if (Math.hypot(lng - prevLng, lat - prevLat) >= minDistDeg) out.push(ring[i]); } // 抽稀后少于 4 个点构不成多边形,返回 null 由调用方丢弃 if (out.length < 4) return null; out[out.length - 1] = out[0]; // 强制首尾闭合 return out; }

阈值minDistDeg的量级要心里有数:0.01度在赤道附近约 1.1 公里,0.05度约 5.5 公里。做世界级大屏用0.05到0.1都很安全,把世界总图从几 MB 压到几百 KB 是常事。Math.hypot算的是欧氏距离,在经纬度空间里不是真实距离,纬度越高误差越大,但这里只是控制点的疏密,不做精确测量,够用。真要精确,就把纬度乘上cos(lat)做修正。

抽稀之后还会有一批「点数很少的小岛」,它们在世界地图上根本看不见,但每个都要生成一条 SVG 路径,纯属负担。丢掉的判断很简单:

// 剔除面积过小的环:用点数和包围盒对角线双重判断 function isTinyRing(ring) { if (!ring || ring.length < 5) return true; // 少于 5 个点(含闭合点)一律丢 let minLng = 180, maxLng = -180, minLat = 90, maxLat = -90; ring.forEach(([lng, lat]) => { minLng = Math.min(minLng, lng); maxLng = Math.max(maxLng, lng); minLat = Math.min(minLat, lat); maxLat = Math.max(maxLat, lat); }); return Math.hypot(maxLng - minLng, maxLat - minLat) < 0.05; // 包围盒太小 }

这里用包围盒对角线代替真正的面积计算,是为了省掉鞋带公式那套逻辑,对大屏场景精度足够。要注意的是只对内部环和孤岛这么干,别把主轮廓也误删——主轮廓的点数天然多,不会被ring.length < 5命中,但如果某个国家的本土被切得很碎,可能整个要素都被判成小环。稳妥的做法是先跑一遍统计,看看被丢弃的要素有多少、都是哪些,再决定阈值。

4.2 按国家切分并落盘

很多业务只关心几个重点国家,不需要全世界的数据。把这些国家的要素从总图里挑出来,单独存成文件,好处是前端按需加载——首屏只拉总图,点进去再拉单个国家,网络请求的粒度刚好匹配交互。

const fs = require('fs'); const path = require('path'); const world = JSON.parse(fs.readFileSync('./data/world.geo.json', 'utf8')); const outDir = './out/countries'; fs.mkdirSync(outDir, { recursive: true }); // 按 ISO 两位码分组,同一个国家的多个多边形要素合并到一个 FeatureCollection const groups = new Map(); for (const feature of world.features) { const key = feature.properties.iso_a2; if (!key || !feature.geometry) continue; // 没有国家码或没有几何的要素跳过 if (!groups.has(key)) groups.set(key, []); groups.get(key).push(feature); } for (const [key, features] of groups) { const fc = { type: 'FeatureCollection', features }; const file = path.join(outDir, `${key}.geo.json`); fs.writeFileSync(file, JSON.stringify(fc)); } console.log(`已生成 ${groups.size} 个国家文件`);

fs.mkdirSync(..., { recursive: true })的recursive参数不能省,否则目录存在时会抛异常,脚本第二次跑就挂。Map而不是普通对象,是因为国家码虽然是字符串,但用Map能保证插入顺序,产出文件的顺序稳定,方便做 diff 对比。分组后每个国家的文件是独立的FeatureCollection而不是裸的features数组,这样任何按标准 GeoJSON 解析的工具都能直接吃。

注意:切分后的分国家文件里的坐标还是原始坐标系(通常是 WGS84 经纬度),如果你要叠加业务自有的底图,先确认底图用的坐标系,不一致的话需要在服务端做一次转换,别在前端硬凑。

4.3 三个坐标约定,弄错就是空白地图

这一节说的三条,都是我在不同项目里真实遇到过的,现象千奇百怪,原因都指向同一处。

第一,GeoJSON 里坐标是[经度, 纬度],不是[纬度, 经度]。反了的话,本来在东半球的国家会跑到南半球去,整张地图看起来像被扔进了搅拌机。判断方法很直接:打印几个坐标,经度范围是-180~180,纬度范围是-90~90,如果你看到某个值超过了 90,那一定是顺序反了。这个坑之所以常见,是因为很多地图 API 的入参是(lat, lng)顺序,和 GeoJSON 相反,抄代码时最容易带错。

第二,环必须首尾闭合。规范要求每个环的第一个点和最后一个点相同,但有些来源的数据为了省字节会省略最后一个点。渲染层如果不做闭合处理,就会出现「每个国家都缺一条边」的诡异画面。校验代码很简单,遍历每个环比较首尾:

// 校验并修复未闭合的环 function closeRings(geometry) { const fixRing = (ring) => { const [first] = ring; const last = ring[ring.length - 1]; if (first[0] !== last[0] || first[1] !== last[1]) return [...ring, first]; return ring; }; if (geometry.type === 'Polygon') { return { ...geometry, coordinates: geometry.coordinates.map(fixRing) }; } return { ...geometry, coordinates: geometry.coordinates.map(poly => poly.map(fixRing)) }; }

第三,内环是洞,不是独立区域。一个Polygon的coordinates是「环的数组」,第一个是外轮廓,后面的都是要挖掉的洞。有些数据源把湖、内陆海单独拆成一个要素,有些则塞在内环里,两种表示在渲染上没有区别,但做面积统计、做点在多边形内判断的时候,内环必须额外处理,否则会把湖面算进国土面积。

还有一个不常见但致命的:跨 ±180 度经线的多边形。一个横跨日期变更线的区域,如果直接按坐标连线,会在图上画出一条横贯整个画面的长条。处理办法是在经度跨越处把多边形切开,或者把负经度整体加 360 后再统一归一化。这个问题出现的概率不高,但一旦碰上,画面会非常刺眼。

5. 常见问题排查:六个我踩过的坑

地图这块的问题有个共同点:报错信息极少,多数时候控制台一片安静,屏幕上就是不对。所以排查要靠「现象反推原因」,下面这几条按我遇到的频率排。

现象一:地图画出来了,但所有国家都是同一个颜色,业务数据完全没生效。原因是染色时用的键和地图要素的键对不上。业务数据里的国家标识常常是名称(「中国」「美国」)或者三位码,而地图要素的properties.iso_a2是两位码,直接拿名称去查colorMap必然查不到,全部走到默认色。 解决办法是建一张映射表,把业务侧的标识统一转成地图侧的 ISO 码,转换放在数据层做,别在渲染循环里做字符串匹配。加一行兜底日志:if (!colorMap[key]) console.warn('未匹配:', key),跑一次就能把没对上的全揪出来。

现象二:整个 SVG 只有一团黑或者一个三角形。九成是坐标顺序反了,[lng, lat]被当成[lat, lng]处理。因为投影函数对超出范围的值不会报错,它只是算出一个诡异的坐标,所有点被压到画布边缘,视觉上就是一团。 解决办法是打印前几个坐标,检查取值范围。经度绝对值超过 90 的点如果大量出现,基本可以确诊。修复就是在读数据时统一交换一次,别在投影函数里加判断,那样会把正确的数据也搞错。

现象三:点击下钻没反应,控制台报「地图未注册」或类似的错误。用图表库时,注册地图是一次性的,同名地图重复注册不一定生效,尤其是下钻时用同一个名字覆盖注册。 解决办法是每次注册用带后缀的唯一名字(比如world和world_CN),或者在下钻前先显式注销旧地图。用原生 SVG 渲染就没有这个问题,这也是我在下钻层级深的项目里更愿意自己写渲染的原因——没有黑匣子,出了错能一路跟到投影函数。

现象四:首屏白屏好几秒,网络面板里地图文件显示 20MB+。原因是用了未经精简的原始数据,加上服务器没开 gzip。JSON 文本的压缩率很高,gzip 之后通常只剩十分之一。 解决办法分两步:先在构建期跑一遍 4.1 的截断和抽稀,把文件压到 1MB 以内;再确认服务器对.json和.geo.json返回了Content-Encoding: gzip。有些静态服务器的默认 mime 表里没有.geo.json,当成application/octet-stream处理,压缩就不生效,需要手动加一条规则。

现象五:地图上出现横贯全图的长条,或者某个国家多了一块奇怪的矩形。跨 ±180 度经线的多边形没有切开,坐标从179直接跳到-179,直线连过去就横穿了整个画布。 解决办法是在数据加工阶段检测相邻点的经度跳变,超过 180 度就在那里断开,把一个多边形拆成两个。检测逻辑很轻:遍历环上的相邻点,Math.abs(lng[i] - lng[i-1]) > 180就记一个切分点。

现象六:本地双击 HTML 打开,地图不出来,控制台报跨域或者 fetch 失败。file://协议下浏览器禁止发起网络请求,这是安全策略,不是代码 bug。 解决办法就是 3.1 写的,起一个本地静态服务。这个坑我吃过不止一次,每次换一台新机器、新建一个 demo 目录就忘了,后来干脆在项目根目录放一个serve.sh,省得每次都去查命令。

6. 二次开发:把散装 JSON 变成能维护的数据流水线

地图数据有个特点:它不像代码那样天天改,但一旦要改,往往是全量替换。如果只是把几个 JSON 文件扔进public/data就完事,半年后数据更新,谁也不知道当初那份是从哪来的、做了什么处理、哪些字段被删过。我后来的做法是把加工过程固化成脚本,数据只从源头进、只从产物出,中间步骤都能重跑。

具体就是把这一章前面的处理串成 npm script:

脚本命令做什么产物
npm run map:inspect统计要素数、几何类型、坐标点总数控制台报告
npm run map:build截断小数位、抽稀、丢小环dist/world.geo.json
npm run map:split按 ISO 码拆分分国家文件dist/countries/*.geo.json
npm run map:validate校验闭合、范围、空几何退出码非 0 即失败

map:build和map:split之间要有依赖顺序,用&&串起来即可。map:validate是这里面最值得写的一个,它把「数据对不对」从人眼检查变成机器检查:

// validate.js —— 在 CI 里跑,退出码非 0 就阻断发布 function validate(fc) { const errors = []; fc.features.forEach((f, i) => { if (!f.geometry) return; // 占位要素跳过 const polys = f.geometry.type === 'Polygon' ? [f.geometry.coordinates] : f.geometry.coordinates; polys.forEach((poly, pi) => { if (!poly.length) errors.push(`#${i} 多边形 ${pi} 没有环`); poly.forEach((ring, ri) => { const first = ring[0], last = ring[ring.length - 1]; if (first[0] !== last[0] || first[1] !== last[1]) { errors.push(`#${i} 环 ${pi}-${ri} 未闭合`); } ring.forEach(([lng, lat]) => { if (lng < -180 || lng > 180 || lat < -90 || lat > 90) { errors.push(`#${i} 坐标越界: ${lng},${lat}`); } }); }); }); }); return errors; } const errors = validate(JSON.parse(require('fs').readFileSync('./dist/world.geo.json', 'utf8'))); if (errors.length) { console.error(`校验失败,共 ${errors.length} 处问题,前 5 条:\n` + errors.slice(0, 5).join('\n')); process.exit(1); } console.log('校验通过');

这个函数只做三件事:环有没有闭合、坐标有没有越界、有没有空多边形。看着简陋,但能拦住我遇到过的绝大多数「数据换了之后地图歪了」的问题,而且是在 CI 阶段拦住,不用等用户来告诉你。

改造方向上,还有两处值得往下走。一是把地图数据和业务数据彻底分开:地图只负责「形状和位置」,业务数据单独一个接口,前端拿 ISO 码做 join,这样换数据源时业务代码一行不改。二是如果项目里下钻层级超过两层(世界 → 国家 → 省州),把分国家文件再拆一级,用countries/CN/这样的目录嵌套,配合路由懒加载,首屏只拉总图、点一层拉一层。别忘了在meta.json里补上每个下级区域的中心点坐标,否则标签会全挤在边界上。

说到教训,最实在的一条是:我最早把加工脚本写在 Jupyter 里跑了一遍就把结果拷进项目,三个月后要调精度,回头找不到当时的阈值设的是多少,只能对着成品文件反推。从那以后我每次处理地图数据,哪怕只是截个小数位,也强制把参数写进脚本、把脚本提交进仓库、把产物目录加进.gitignore——数据可以重新生成,但参数丢了就是后悔药都买不到。

希望这份拆解能帮到你,把手里那包 JSON 真正用起来。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询