简介:这是一份基于百度天气接口与Leaflet技术的湖南空气质量WebGIS可视化可运行源码,适合WebGIS初学者、前端开发者及环境数据展示爱好者参考。资源完整实现了从天气数据获取到地图图层渲染的轻量级流程,包含湖南省内中、重污染地区与优质地区的空间分布展示,以及污染严重和空气质量优的前10名区县排名,可直观用于区域空气质量对比与可视化方案验证。压缩包共3个文件,以index.html为主页面,搭配.inscode运行配置与.gitignore版本管理说明,整体仅7KB,结构精简,便于快速部署和二次修改。目前已46人学习,对于想了解百度天气接口调用、Leaflet地图集成及省级空气质量可视化实现思路的开发者,是一份值得直接运行的示例代码。
1. 湖南空气质量WebGIS可视化:把监测数据落到网页地图上的完整方案
当同事把“这周湖南空气质量明显变差的区域是哪里”这个问题扔过来时,Excel 做不出空间化的答案。湖南空气质量WebGIS可视化这套可运行源码,解决的就是这类问题:把散落在国控站点、地市站点的 AQI 与六项污染物数据,按经纬度投影到一张网页地图上,用气泡颜色和大小直观反映空间分布,再靠点击弹窗展示单点浓度。整套方案以轻量地图引擎加一个本地服务为主,浏览器打开就能用,放到服务器上就能给内网同事访问。适合要把监测数据变成可视化页面的开发者、环保业务人员,以及做数据展示场景的在校学生。
2. 湖南空气质量WebGIS的架构拆分:从数据文件到网页地图的完整链路
一份可运行的空气质量 WebGIS 源码,本质上是四件事:监测数据怎么存、地图边界从哪来、数据如何通过接口出去、页面怎么渲染。浏览器里的 index.html 只负责最后一环,中间任何一环断掉,页面上都看不出问题出在哪。所以拿到源码后先别急着跑,把这条链路捋清楚,后面排错会快很多。
2.1 监测数据怎么组织:站点、坐标与AQI字段的最小约定
数据文件的组织方式决定了后面所有代码的写法。常见的做法是把监测站信息存成 JSON 数组,每条记录代表某个站点在某个时次的监测结果。我见过不同版本的源码字段名略有出入,但以下这些字段是绕不开的:
| 字段 | 类型 | 示例值 | 说明 |
|---|---|---|---|
| station_id | string | A101 | 站点唯一编号,前端弹窗和图表联动都靠它 |
| name | string | 湘北-A站 | 展示名称,建议带方位或行政区前缀 |
| lon | number | 112.13 | 经度,十进制度数,WGS84 坐标 |
| lat | number | 28.22 | 纬度,同上 |
| aqi | number | 85 | 综合空气质量指数 |
| pm25 | number | 62 | PM2.5 浓度,按项目需要可扩展其他污染物 |
| time | string | 2026-01-15 10:00 | 监测时次,推荐本地时间字符串,精确到分钟 |
经纬度要存十进制度数,不要存度分秒格式,地图库默认都认小数。精度上保留小数点后 5 位大约对应 1 米的分辨率,对国控站点展示完全够用;如果数据里坐标只有 3 位小数,站点位置会明显毛糙甚至跨街区偏移,拿到数据先做一次抽样核对。时间字段建议直接存“本地时间字符串”,格式要严格到空格都对齐,因为后面做时间维度切换时用到的往往是字符串匹配而不是日期解析,格式不一致会查不出数据。
坐标系是这个环节最大的暗坑。站点数据可能来自不同数据平台,有的给 GPS 坐标,也就是 WGS84,有的给国内地图坐标,也就是 GCJ-02。两种坐标系在湖南区域大概有几百米的差异,混装后会呈现“一部分点正常、一部分点偏移”的诡异现象。组织数据阶段就要确认每个字段的坐标系来源,并在文件头部注释写明,后续统一换算。
2.2 地图选型:为什么可运行源码里通常选Leaflet而不是3D引擎
地图渲染层有多个选择,但这类站点级可视化最常见的是 Leaflet,其次是 OpenLayers,再往上才是 Cesium 这类三维引擎。
| 引擎 | 适合场景 | 上手成本 | 本项目取舍 |
|---|---|---|---|
| Leaflet | 点线面展示、专题图、几十个图层以内 | 低,熟悉 API 后半小时能出图 | 首选,体积小 API 直接 |
| OpenLayers | 多投影切换、复杂几何运算、大量图层管理 | 中高,API 偏底层 | 功能过剩 |
| Cesium | 三维地形、大场景时空连续 | 高,需要补 WebGL 概念 | 杀鸡不用牛刀 |
站点的空间分布用圆形标记就够表达,不涉及三维视角、地形加载和连续轨迹动画,Cesium 的立体效果在这里没有用武之地。OpenLayers 的强项在于投影切换和复杂空间运算,而本项目只有一种投影、一类点数据,用 Leaflet 能让代码量少一大截。图表部分用 ECharts 承担排名表和单站污染走势图,它和地图之间通过点击事件传递站点编号联动,两个库各管各的,不混写。
底图部分常见做法是直接用开源瓦片服务的 URL 模板,代码量最小。国内访问这类瓦片服务时快时慢,这属于网络环境问题,不是代码缺陷;要彻底解决可以改用离线瓦片包。如果你换了国内地图厂商的瓦片,就必须注意 GCJ-02 与 WGS84 的偏移问题——底图是加密坐标,站点数据是原始坐标,两者叠加位置对不上,这就是后面避坑章节要展开的经典现象。
还有一个常见疑问是为什么不用 WebSocket 做实时推送。这类空气质量可视化场景的数据更新粒度通常是小时级甚至天级,用户重新打开页面拉一次最新数据就够了,实时推送把架构复杂度抬高,却没有带来体验收益。可运行源码里维持 HTTP 轮询是足够且合理的。
2.3 一套常见目录结构:拿到源码后先看哪几个关键文件
下面是一套常见的源码目录约定,不一定每个包都长这样,但大概率是这个骨架:
project/ ├─ public/ # 前端页面文件 │ └─ index.html # 页面入口,含地图初始化、图层渲染代码 ├─ data/ # 本地数据文件 │ ├─ hunan_city.geo.json # 湖南省地市级边界,GeoJSON 格式 │ └─ latest.json # 最新一期监测站点数据 ├─ server.js # 本地 HTTP 服务入口 └─ package.json # 依赖声明拿到源码先做三件事:第一,打开 package.json 看 dependencies,确认是不是只有 express 这类轻量依赖,有没有明显不相关的重组件;第二,看 data 目录,边界文件和站点数据文件是否真实存在,各有多大;第三,看入口文件注释或配套说明里写的启动命令。如果 data 目录是空的,通常说明数据需要单独下载或由接口动态写入,这时候服务能起来但页面空转,不代表程序坏了。
另外要确认边界文件的粒度。如果里面是街道甚至村级边界,页面也许还能跑,但拖动缩放会明显卡顿,这个坑在第 4 章详细展开。省级空气质量可视化用市级边界足够,既保留空间辨识度,又不拖累交互性能。边界文件和站点数据文件统一放在 data 目录,便于后续替换与增量更新。
3. 把数据画上湖南地图:核心代码与必调参数
这一章把前端渲染拆成四步:初始化地图、加载湖南边界、绘制站点、绑定弹窗,最后用本地服务把整套串起来。代码是可直接运行的示例,参数按湖南区域的常规范围预设,你可以直接复制到自己的页面里改。
3.1 初始化地图与加载湖南边界:两个JSON缺一不可
先初始化地图画布并加载湖南地市级边界。这一步的前提是 data 目录里存在边界 GeoJSON 文件,没有它地图上就只有底图,没有省界参考。
// 初始化地图,中心点取湖南地理中心附近,缩放级别选 7 能看全全省 const map = L.map('map').setView([28.2, 111.7], 7); // 底图使用开源瓦片服务,其投影坐标系为 WGS84 L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 18 }).addTo(map); // 加载湖南地市级边界,文件放在 data/hunan_city.geo.json fetch('data/hunan_city.geo.json') .then(res => res.json()) .then(geo => { L.geoJSON(geo, { style: { color: '#555', weight: 1.2, fill: false } }).addTo(map); });setView 的第一个参数是 [纬度, 经度] 数组,不要写反,这是新手最常见的低级错误。湖南的经纬度范围大致是东经 108 到 114 度、北纬 24 到 30 度,中心点取 28.2, 111.7 再配缩放级别 7,正好铺满整个视口。底图 URL 模板里的{s}、{z}、{x}、{y}占位符由 Leaflet 自动替换,不需要你处理。style 对象里把 fill 设为 false,只保留描边,能省去边界内部的填充渲染,让站点气泡更突出。
3.2 AQI分级着色:颜色映射与气泡半径的代码写法
AQI 按国标数值分成六级,每一级有约定俗成的标识色。可视化项目里最常见的翻车方式是把颜色写成线性渐变,导致轻微污染和中度污染在图上色阶接近,阈值处体现不出等级跳跃。正确做法是分段函数。
| AQI 范围 | 等级 | 颜色 |
|---|---|---|
| 0 - 50 | 优 | #00e400 |
| 51 - 100 | 良 | #ffff00 |
| 101 - 150 | 轻度污染 | #ff7e00 |
| 151 - 200 | 中度污染 | #ff0000 |
| 201 - 300 | 重度污染 | #99004c |
| 300 以上 | 严重污染 | #7e0023 |
对应代码:
// 按国标分级映射颜色,逐级拦截,边界条件用 <= function aqiColor(aqi) { if (aqi <= 50) return '#00e400'; // 优 if (aqi <= 100) return '#ffff00'; // 良 if (aqi <= 150) return '#ff7e00'; // 轻度污染 if (aqi <= 200) return '#ff0000'; // 中度污染 if (aqi <= 300) return '#99004c'; // 重度污染 return '#7e0023'; // 严重污染 } // 气泡半径:线性映射后封顶,防止数值悬殊导致图面失衡 function aqiRadius(aqi) { return Math.min(6 + aqi / 60, 20); }颜色函数的判断顺序是从小到大拦截,aqi=50 会命中第一个分支返回绿色,aqi=51 落入第二个分支,判断链不会出现重叠区间。半径函数里 6 是保底像素,20 是上限,这样 AQI 为 30 和 AQI 为 300 的站点在图上大小差异明显但不至于一眼看成两类数据。若站点密度高,可把系数 aqi / 60 调低到 aqi / 80,并同步降低封顶值,避免大面积气泡互相遮盖。
3.3 站点弹窗与时间切换:让地图能“讲”出数据变化
站点绘制用圆形标记而不是图标标记,原因是圆形标记的颜色和半径可以动态绑定数值,而图标标记换色要维护一堆图片。用图层组统一管理站点,切换时间时批量清理重建。
// 统一管理站点图层的图层组,便于整体清空与重建 const stationLayer = L.layerGroup().addTo(map); function addStationMarker(s) { L.circleMarker([s.lat, s.lon], { radius: aqiRadius(s.aqi), color: '#ffffff', weight: 1, fillColor: aqiColor(s.aqi), fillOpacity: 0.8 }) .bindPopup( `<b>${s.name}</b><br>AQI: ${s.aqi}<br>PM2.5: ${s.pm25}<br>时间: ${s.time}` ) .addTo(stationLayer); } // 时间维度切换:重建站点图层,几十个站点时性能足够 function renderTime(datetime) { stationLayer.clearLayers(); const records = allRecords.filter(r => r.time === datetime); records.forEach(addStationMarker); } timeSelect.addEventListener('change', e => renderTime(e.target.value));使用 L.layerGroup 管理站点图层,切换时先 clearLayers 再逐个重建,能避免旧时次的点残留在地图上。fillOpacity 取 0.8 在站点密集时既看清重度污染点的实色,又不至于完全盖住边界线。时间字段的匹配是全等匹配,2026-01-15 10:00和2026-01-15 10:00带个空格就是两条数据,一旦切换后站点消失,先检查时间字符串格式是否完全一致。站点数量超过几百个时,重建图层的做法会有一点性能压力,但大多数空气质量可视化场景远到不了这个量级。
3.4 起一个本地服务跑通整套源码:避免双击HTML的跨域坑
很多同学拿到源码第一反应是双击 index.html,结果页面能显示底图却没有站点数据。原因是浏览器在 file:// 协议下禁止 fetch 本地文件。底图能显示是因为它走的是远程瓦片 URL,而本地 JSON 文件属于跨域读取。解决方式就是起一个本地 HTTP 服务,这也是可运行源码里通常会带 server.js 的原因。
const express = require('express'); const path = require('path'); const fs = require('fs'); const app = express(); const PORT = 8080; // 前端静态文件目录 app.use(express.static(path.join(__dirname, 'public'))); // 数据接口:读取 data/latest.json 返回给前端 app.get('/api/aqi', (req, res) => { const json = JSON.parse(fs.readFileSync('./data/latest.json', 'utf-8')); res.json(json); }); // 监听 0.0.0.0 使局域网内其他机器也能访问 app.listen(PORT, '0.0.0.0', () => { console.log(`服务已启动: http://localhost:${PORT}`); });这个服务只做两件事:托管 public 目录下的静态文件,以及提供 /api/aqi 数据接口。启动流程是先npm install express再node server.js,浏览器访问 http://localhost:8080。如果你把端口改成其他值,注意前端代码里请求地址也要跟着改,否则接口 404。需要给局域网同事演示时,监听地址必须写成 0.0.0.0,这样本机 IP 加端口才能被外部访问,默认监听 localhost 外面是进不来的。
4. 湖南空气质量可视化源码的避坑清单:坐标、边界、跨域与性能
源码跑通只是第一步,显示对才是硬道理。地图这类应用出错时往往不报错,只是位置不对、颜色不对、加载慢,排查起来全靠经验。下面五条是我在实际运行这类源码过程中遇到的典型问题,每一条都按现象、原因、解决的顺序拆开讲。
4.1 站点位置整体偏移几十到几百米
现象:所有站点都落在底图上的错误位置,偏移方向一致,单个点看不出问题,整体看就像地图平移了一段距离。我之前遇到的一个典型场景是,某站点明明在路东侧,气泡却跑到路西侧的街对面。
原因:站点数据来自国内地图生态,坐标是 GCJ-02 加密坐标,而 OSM 底图是 WGS84 坐标。两者在湖南区域存在几百米的系统性偏移。数据源没声明坐标系,或声明了但没做转换,就直接画点。
解决:先确认数据坐标来源,再决定是否转换。如果确认是 GCJ-02,就需要在绘制前把经纬度换算成 WGS84,常见做法是引入一个小型坐标转换函数库,调用转换方法批量处理。不要自己套用网上流传的固定偏移公式,那些近似参数在不同纬度误差不一致。转换完成后抽样两三个站点与卫星底图或路网对照,确认没有转反,GCJ-02 转 WGS84 和反向操作容易搞混。
4.2 边界跑到图外甚至变形扭曲
现象:底图显示正常,但湖南边界轮廓整体偏移或旋转,严重时跑到地图边缘外;有的边界还和底图上的省界线明显不重合,像是叠了一层错位的蒙版。
原因:边界 GeoJSON 本身不是标准 WGS84 坐标。有些数据源导出时保留的是地方坐标系或加密坐标系,叠加到 WGS84 底图上自然错位。另一种情况是 GeoJSON 里混入了投影参数,而前端没有做任何投影转换。
解决:优先更换数据源,用 WGS84 的省市级 GeoJSON。换完数据后可以用 mapshaper 打开文件检查坐标范围,正常应该在东经 108 到 114、北纬 24 到 30 之间,如果出现几百上千的数值,大概率不是经纬度坐标。不要尝试在前端做投影转换,那会让问题复杂化,直接换数据文件是最快的出路。
4.3 双击HTML打开,页面有地图却没有站点数据
现象:用浏览器直接打开 index.html,底图和边界正常显示,但站点气泡缺失。打开开发者工具的控制台,能看到红色的 Failed to fetch 报错。从同事那里拷来的源码,自己跑就是少数据,很让人怀疑是代码被改坏了。
原因:浏览器在 file:// 协议下对本地文件读取有严格限制,fetch 请求指向本地 JSON 会被浏览器安全策略拒绝。远程底图不受影响,因为它本身是一次正常的 HTTP 请求,所以页面看起来“部分正常”。
解决:按 3.4 的方式起本地 HTTP 服务,通过 http://localhost 访问页面,这是推荐做法。如果只想临时看效果,也可以把数据直接内联成 JavaScript 变量写进页面,双击就能打开,缺点是一次性数据,更新要重新生成 HTML,不适合做后续的每日更新版本。
4.4 地图缩放卡顿,拖动像掉帧
现象:页面加载完成后地图能显示,但鼠标拖动时边界和站点明显迟滞,缩放手感很差,CPU 占用率飙升。边界线条复杂的地方卡顿尤其明显。
原因:边界 GeoJSON 包含的顶点数过多。省级可视化如果用了乡镇级甚至村级边界,顶点数轻松上万,每次缩放重绘都要重新计算和绘制这么多点,浏览器扛不住。很多下载到的公开边界数据原始精度极高,直接使用就会踩这个坑。
解决:在数据源头做简化。mapshaper 是处理 GeoJSON 简化最顺手的工具,命令行一条就能出结果:
# 简化边界,保留约15%的顶点,肉眼几乎看不出变形 npx mapshaper hunan_raw.geo.json -simplify 15% -o dist/hunan_city.geo.jsonsimplify 后面的 15% 表示保留约 15% 的顶点,适合省级轮廓展示;如果只关注地市局部,可以进一步降到 10%,文件更小加载更快。简化后再对比一下和原始边界在重点地市的形状差异,只要市级轮廓没丢角就没问题。此外,前端绘制时把 fill 设为 false,只画描边也能减轻渲染负担。
4.5 AQI颜色和官方发布不一致
现象:某天某个站点 AQI 为 120,属于轻度污染,地图上却显示深红色,看起来像重度污染区域。当天官方 App 里显示的是橙色,两边颜色对不上,业务人员质疑数据出了错。
原因:颜色映射写成了线性渐变,比如把 AQI 从 0 到 300 均匀映射到红黄绿,导致阈值附近没有等级跳变,轻度污染和中度污染在视觉上区分不开。也有些代码把国标色值记混,轻度污染用了黄色,中度用了橙色,整个色阶后移了一档。
解决:用 3.2 的分段函数,且色值严格按国标约定。轻度污染就是 #ff7e00 橙色,中度才是 #ff0000 红色。判断边界时注意整数比较,AQI=100 属于良,AQI=101 属于轻度污染,代码里<= 100和<= 150的连续判断天然满足这个边界,不要改成<,否则整条色阶在临界点都会错位。
5. 从静态Demo到每日更新:给这套源码接上真实数据源
源码跑通只是开始,真正让它有价值的是数据自动更新。手工下载数据再替换 JSON 的方式坚持不了几天,可运行源码要落地成日常可用的小系统,至少得把定时拉取、归档、部署这三件事做到位。
5.1 定时拉取接口数据并归档
常见做法是在服务端加一个定时任务,每天拉取一次最新监测数据,写入两个文件:一个是供前端读取的 latest.json,一个是按日期命名的归档文件。
const cron = require('node-cron'); const endpoint = 'https://你的数据服务地址/api/air/hourly'; // 每天0点执行:拉取最新数据,写入最新文件并归档 cron.schedule('0 0 * * *', async () => { const res = await fetch(endpoint); const json = await res.json(); const day = new Date().toISOString().slice(0, 10); fs.writeFileSync(`./data/${day}.json`, JSON.stringify(json)); fs.writeFileSync('./data/latest.json', JSON.stringify(json)); });cron 表达式0 0 * * *表示每天零点执行,前两位分别是分和时,后面分别是日、月、周。Node.js 18 以上可以直接用全局 fetch,旧版本需要引入 node-fetch。归档文件按日期命名后,前端时间轴可以直接把文件目录列表映射成可选择的日期,形成历史回看。注意服务器时区要调整到东八区,否则归档文件名和本地时间对不上。
5.2 前端加一个时间轴回看
归档文件有了,前端要做的事情很简单:把日期列表渲染成下拉框或滑块,切换时把 5.1 生成的归档文件作为数据源传入 3.3 里的 renderTime 函数。这里的核心约束是归档数据和最新数据的字段结构必须完全一致,时间字段格式也要统一。如果某一天拉取失败,归档目录里会缺一个文件,前端要有兜底提示,比如“该日数据缺失”,而不是静默空白。
5.3 部署成常驻服务
本地跑通后要部署到服务器,用 PM2 守护 Node 进程是最省事的方案。启动命令是pm2 start server.js --name air-map,然后执行pm2 save和pm2 startup让服务开机自启,这样即使服务器重启,地图服务也会自动回来。内网用户通过本机 IP 加端口访问,3.4 里的监听地址 0.0.0.0 在这时就起作用了。
我第一次部署这类地图服务时,忘记给数据接口设置缓存控制头,第二天打开页面看到的还是浏览器缓存的旧数据,排查了一上午才意识到是缓存问题。后来养成了习惯:凡是每天更新的数据接口,要么在响应头显式设置 Cache-Control: no-cache,要么在请求 URL 后加时间戳参数。这套源码本身不复杂,但把更新链路跑顺、把缓存和时区这些细节管好,才算真正能用起来。希望帮到你。
本文还有配套的精品资源,点击获取