资讯详情

GEE开发实战:点击地图查询最近发电站信息

📅 2026/10/12 23:36:16 | 华诺云谱 👁 阅读
GEE开发实战:点击地图查询最近发电站信息
做能源选址、电网规划或者环境评估这行的朋友应该都遇到过这种场景手头有一份某个区域的发电站列表领导或者同事突然问你——离某某位置最近的电站是哪座装机容量多大烧的是什么燃料以前我的常规操作是打开本地GIS软件导CSV、建图层、跑空间连接、再手动算距离一套流程下来怎么也得十几二十分钟如果数据在别人手里还得先要文件、对字段烦得很。后来我搭了一个Google Earth EngineGEE的小应用打开页面就是一张交互地图点地图上任意一个位置左侧面板自动显示离这个点最近的一座发电站名称、装机容量、燃料类型、距离这些信息直接列出来地图上还把最近电站和连线高亮画出来。整个过程从点一下到看到结果大概两秒钟。这个应用本质上就是一个点选位置 → 最近点距离计算 → 信息回显的模板换成学校、医院、基站、充电桩任何点状数据都能复用。这篇文章就是把这个小APP从需求拆解、数据准备、距离计算到面板搭建的全过程记录下来包含完整可抄的代码框架和各种我踩过的坑。适合用GEE做地理分析、想快速搭建内部可视化工具或者对WebGIS开发感兴趣的开发者参考。1. 这个APP到底解决什么问题从需求倒推设计1.1 一句话需求背后的三个关键词标题里的最近点距离点是理解整个项目的钥匙。拆开来看就是三个要素一个候选点集合发电站、一个目标点你点击的位置、一套距离计算规则最近邻。整个APP要干的活就是目标点确定后在候选点集合中找出距离最小的那个并展示它身上的属性信息。这个需求看起来简单但在传统GIS工具里做一次并不轻松。首先是数据获取发电站数据集往往是几十MB的CSV或者Shapefile得下载、导入、检查投影然后是距离计算要新建字段、写表达式、跑一次空间连接整个过程还必须守着桌面软件操作最后是展示做完的分析结果想给别人看要么截图要么打包工程文件没法形成一个打开就能用的共享工具。GEE天然解决了这几个痛点。数据在云端不用下载空间算子直接在服务器上批量跑几万个点的距离矩阵就是一条语句的事UI组件和地图组件可以直接组装成网页应用发布之后给个链接别人就能用不需要对方装任何GIS软件。这也是我在这类小工具场景里首选GEE而不是本地方案的原因。1.2 为什么是GEE而不是传统GIS或者纯前端可能有人会问用Leaflet或者OpenLayers调一个后端接口前端画个Marker不是也能实现吗能实现但工程量大得多。你要准备后端服务、地理数据库、距离计算接口、前端部署还要考虑并发和数据更新。而GEE把这几层全部打包了数据存储在你自己的asset里或者公开数据集中空间计算引擎在云端前端UI有现成的组件最后还能一键发布成App。对于内部工具或者原型验证这几乎是成本最低的路径。当然GEE也有它的短板不适合做高并发的大众产品UI的灵活度不如纯前端框架高。但最近点距离点这种低频交互、数据量中等、结果要清晰展示的场景恰好是它的舒适区。1.3 整体架构视图、逻辑、数据三层分离我习惯把这类小应用分成三层来设计后面写代码时思路会特别清爽视图层地图组件、左侧面板、按钮、下拉框负责用户交互和结果展示。逻辑层点击事件触发后的距离计算、最近点筛选、结果格式化全部跑在GEE的Server端。数据层发电站矢量数据集可以来自GEE公开数据集也可以上传自己的CSV/Shapefile。这样的分层还有一个好处换数据源时只需要改数据层和字段映射视图和逻辑基本不动。后面我在第4章给的代码骨架也是按这个顺序组织的。2. 数据准备与最近点距离核心算法2.1 发电站数据从哪来选数据集先说需求GEE里能直接用的全球发电站数据最典型的是全球发电厂数据库GPPD的社区公开版本。这份数据覆盖全球几十个国家的发电设施每条记录带经纬度、装机容量、主要燃料类型、所属国家等字段。字段关键但不多做信息回显刚好够用。如果你要分析的区域没有现成公开数据完全可以把自己的CSV传上去——GEE资产面板里直接上传列名自己能控制。我个人建议CSV里至少要有x、y或者lng、lat、name、value这几个字段再有一两个分类字段比如燃料类型或者状态。字段越干净后面代码越好写别贪多。加载公开数据集的代码很简单var powerPlants ee.FeatureCollection(projects/sat-io/open-datasets/power_plant_database);注意这个asset id是社区维护版本如果你加载失败去GEE数据目录里搜power plant选一个能用的就行但字段名要自己确认一遍。数据加载后先print(powerPlants.first())看一下字段确认名称、容量、燃料类型对应的属性名后面所有代码都依赖这一步。2.2 最近点距离计算的两种方案及其原理我要重点讲这个部分因为最近点距离点是整个APP的灵魂。GEE里实现点对点最近邻有两种思路我都实际用过。方案一遍历候选点逐个算距离排序取最小思路是把发电站集合里的每个点都用distance()方法算到目标点的距离然后作为新属性存起来最后按距离排序取第一个。代码长这样var targetPoint ee.Geometry.Point([lng, lat]); var withDist powerPlants.map(function(f) { var d f.geometry().distance(targetPoint, 10); // 第二个参数是最大误差容限 return f.set(distance, d); }); var nearest withDist.sort(distance).first();这段代码的逻辑非常直白map遍历所有电站每个电站都算一次到目标点的距离距离字段写进Feature然后排序取第一条。适合候选点数量在几千到几万这个量级计算时间可以接受而且结果可控、方便排查。方案二全量最近邻算子 reduceNearest如果候选点量很大比如几十万甚至上百万个点逐个map遍历的耗时会直线上升。这时候可以用reduceNearest—— 它是GEE里专门做集合到集合最近邻归约的算子。语法上可以这么理解目标点集合调用该算子传入候选点集合它会自动为每个目标点找到距离最近的候选点并且把距离写入结果var nearestFC powerPlants.reduceNearest({ collection: ee.FeatureCollection([targetPoint]), key: distance, maxDistance: 500000 }); var nearest nearestFC.first();maxDistance是搜索的半径上限单位是米。这个参数要设好太小找不到点太大计算量上升。我一般给50公里如果目标区域偏才调大。reduceNearest的底层是空间索引的最近邻搜索不会真的把所有候选点都遍历一遍所以性能远优于方案一。两种方案怎么选我的经验是数据量小于5万、只是想快速验证逻辑用方案一代码可读性强数据量上来、或者以后打算频繁换目标点计算用方案二。本文后面示例代码用方案一为主更容易看懂和调试如果数据量大再换二。2.3 坐标系、单位与距离精度三个坑必须提前说距离计算看着简单但GEE里这三个细节不注意结果会天差地别。坑一经纬度坐标系下算的是大圆距离概念上的近似。GEE的distance()方法返回的是以米为单位的测地线距离内部会自动处理地球曲率所以你不需要手动做投影转换。但要注意它输出的精度受第二个参数maxError影响。这个参数表示允许的最大计算误差你给的越严格内部计算越精细耗时越高。默认值有时候会比较粗我建议显式给个10米性能影响不大精度够用。坑二point不要真当点用。如果你直接拿CSV上传的经纬度构造ee.FeatureCollection再取.geometry()有时候会发现.distance()的结果不是期望的数值。这通常是因为数据里有些记录的坐标格式不对或者字段名没对上geometry实际是空的。我在第5章会专门说怎么排查。坑三筛选要先做。如果数据库覆盖全球而你实际关心的是目标点周边几百公里先用filterBounds把集合切到目标点附近再跑距离计算速度会快一个数量级。这个优化放在任何算法前面var nearby powerPlants.filterBounds(targetPoint.buffer(200000));先buffer一个半径圈出候选区再对这一个小集合算距离逻辑和性能都会好很多。这个方法强烈推荐。3. 面板搭建与交互逻辑从地图点击到信息回显3.1 界面布局左侧控制面板 右侧地图GEE的UI组件用起来很像搭积木。我用的布局很简单根容器是ui.Splitter左边放一个竖着的ui.Panel做控制面板右边是一个ui.Map。控制面板从上到下依次是一段介绍文字、一个下拉框选择燃料类型、一个显示结果的面板初始是隐藏的、一个清空按钮。面板的样式直接用style参数控制宽度、边距、字体大小都写在对象里var panel ui.Panel({ layout: ui.Panel.Layout.flow(vertical), style: { width: 320px, padding: 8px } });这里有个容易忽略的细节ui.Panel.Layout.flow(vertical)指定了子组件垂直排列如果不写默认的布局方式可能会让组件横向挤在同一个面板里。我第一次做的时候就因为漏了这句下拉框和按钮全并排了调试了半天才发现是布局没设置。地图实例直接用ui.Map创建设置中心点和缩放级别var map ui.Map(); map.setCenter(116.0, 35.0, 6); // 先定位到大致区域用户自己拖动3.2 点击地图、计算距离、提取最近电站的完整链路核心交互是用户点击地图任意位置我在地图上加一个图层标记点击点同时触发一次服务端计算。GEE绑定点击事件用的是onClick回调map.onClick(function(coords) { // coords.lon / coords.lat 就是点击的经纬度单位是十进制度 var pt ee.Geometry.Point([coords.lon, coords.lat]); runNearest(pt); });这里有一个新手常见误区coords虽然是个对象但它是客户端对象不是ee.*类型。如果你想把它传给服务端函数用需要先手动包成ee.Geometry.Point我上面的代码就是这么做的。如果不加这一步后面所有计算都会报cannot use a computed geometry in this context之类的错。runNearest是真正干活的函数。它的职责是接收点、计算最近电站、把结果写到面板里的Label上同时更新地图图层。核心逻辑我直接给出function runNearest(targetPoint) { var searchRadius 200000; // 200km var candidates powerPlants .filterBounds(targetPoint.buffer(searchRadius)); var withDist candidates.map(function(f) { var d f.geometry().distance(targetPoint, 10); return f.set(distance, d); }); // 按距离排序取最小 var nearest withDist.sort(distance).first(); nearest.evaluate(function(val) { if (!val) { showResult(null); return; } var props val.properties; showResult({ name: props.name || 未知电站, fuel: props.primary_fuel || 未知燃料, capacity: props.capacity_mw || 未知装机, distanceKm: (props.distance / 1000).toFixed(1) }); }); }这里用到了evaluate—— GEE的客户端回调机制。服务端算出来的结果不能直接当普通JavaScript对象用必须通过evaluate把值取回浏览器端。这是搭建UI面板时绕不过去的一个环节。实际项目里我会把这段封装得再粗糙一点也没关系关键是理解这个异步回调模型。3.3 信息展示与图层可视化结果面板是几个ui.Label我预先在页面上占好位计算完成后更新其valuevar nameLabel ui.Label(, { fontWeight: bold }); var fuelLabel ui.Label(, {}); var capacityLabel ui.Label(, {}); var distanceLabel ui.Label(, { color: #666666 }); function showResult(info) { if (!info) { nameLabel.setValue(未找到电站); fuelLabel.setValue(试试扩大搜索范围); capacityLabel.setValue(); distanceLabel.setValue(); return; } nameLabel.setValue(info.name); fuelLabel.setValue(燃料类型 info.fuel); capacityLabel.setValue(装机容量 info.capacity MW); distanceLabel.setValue(距离 info.distanceKm km); }地图图层这块我会维护几个图层变量一个是全部电站灰点图层一个是点击位置标记图层一个是最近电站高亮图层再加一条从点击点到最近电站的连线。图层的添加方式是用map.addLayer(eeObject, visParams, name)图层之后可以靠map.layers()拿到再设置透明度。连线的实现是构造一条线串几何var line ee.Geometry.LineString([ [clickLon, clickLat], [nearestLon, nearestLat] ]);线宽调成3像素、黄色视觉上非常直观。用户一眼就能看出我点的在这最近电站在那距离多远。这个细节虽然小但是整个工具给外行演示时效果会好非常多。4. 完整代码骨架与关键细节可以直接抄的版本4.1 代码结构总览整个APP我习惯拆成五个部分引入数据、初始化地图面板、定义计算函数、定义展示函数、绑定交互事件。下面这个骨架是去掉异常处理的最小可用版本你可以直接跑通再往里面加功能// 1. 数据层 var powerPlants ee.FeatureCollection(projects/sat-io/open-datasets/power_plant_database); // 2. 视图层 var map ui.Map(); map.setCenter(116.0, 35.0, 6); var panel ui.Panel({ layout: ui.Panel.Layout.flow(vertical), style: { width: 360px, padding: 10px } }); var titleLabel ui.Label(发电站最近点查询工具, { fontSize: 16px, fontWeight: bold }); var fuelSelect ui.Select({ items: [全部, Coal, Gas, Hydro, Wind, Solar], value: 全部 }); var resultWrap ui.Panel({ layout: ui.Panel.Layout.flow(vertical), style: { marginTop: 8px } }); var nameLabel ui.Label(, { fontWeight: bold, fontSize: 14px }); var fuelLabel ui.Label(, {}); var capacityLabel ui.Label(, {}); var distanceLabel ui.Label(, { color: #666666 }); var clearBtn ui.Button(清空结果); resultWrap.add(nameLabel); resultWrap.add(fuelLabel); resultWrap.add(capacityLabel); resultWrap.add(distanceLabel); panel.add(titleLabel); panel.add(fuelSelect); panel.add(resultWrap); panel.add(clearBtn); var splitter ui.Splitter(panel, map); ui.root.add(splitter); // 3. 逻辑层 function runNearest(targetPoint) { var radius 200000; var filtered powerPlants.filterBounds(targetPoint.buffer(radius)); // 如果下拉框指定了燃料类型再加一层过滤 var selected fuelSelect.getValue(); if (selected ! 全部) { filtered filtered.filter(ee.Filter.eq(primary_fuel, selected)); } var withDist filtered.map(function(f) { var d f.geometry().distance(targetPoint, 10); return f.set(distance, d); }); var nearest withDist.sort(distance).first(); nearest.evaluate(function(val) { if (!val) { nameLabel.setValue(该范围内未找到电站); fuelLabel.setValue(); capacityLabel.setValue(); distanceLabel.setValue(); return; } var p val.properties; nameLabel.setValue(p.name || 未知电站); fuelLabel.setValue(燃料类型 (p.primary_fuel || 未知)); capacityLabel.setValue(装机容量 (p.capacity_mw || 未知) MW); distanceLabel.setValue(距离 (p.distance / 1000).toFixed(1) km); // 高亮最近电站 var nearestPt ee.Geometry.Point([p.longitude, p.latitude]); map.layers().set(1, ui.Map.Layer(nearestPt, { color: red, pointRadius: 6 }, 最近电站)); // 画连线 var clickPt val.geometry ? ee.Geometry(val.geometry) : targetPoint; var line ee.Geometry.LineString([[targetPoint.coordinates().get(0), targetPoint.coordinates().get(1)], [p.longitude, p.latitude]]); map.layers().set(2, ui.Map.Layer(line, { color: yellow }, 距离线)); }); } // 4. 交互层 map.onClick(function(coords) { var pt ee.Geometry.Point([coords.lon, coords.lat]); map.layers().set(0, ui.Map.Layer(pt, { color: cyan, pointRadius: 5 }, 点击位置)); runNearest(pt); }); clearBtn.onClick(function() { nameLabel.setValue(); fuelLabel.setValue(); capacityLabel.setValue(); distanceLabel.setValue(); });这段代码直接粘贴到GEE代码编辑器的Apps模式里是可以跑的。跑通之后你会发现一个最关键的经验先用最简单的数据把链路打通再加UI细节。我一开始就是把精力花在界面上结果点击事件和数据计算各种报错界面的问题跟计算的问题混在一起排查了很久。后来先去掉所有UI用print()验证runNearest输出正确再一步步加回面板和图层整个开发过程顺畅多了。4.2 图层管理的两个实用技巧图层管理是这个APP里最容易被忽略的部分。很多人一开始直接map.addLayer(...)成功一次就不管了第二次点击地图时上一个点击位置和上一个最近电站还留在图上新结果和旧结果叠在一起界面非常乱。解决思路是不要无限增加图层而是固定图层槽位。用map.layers().set(index, layer)的方式覆盖指定位置的图层。上面代码里的0号槽位放点击位置、1号槽位放最近电站、2号槽位放距离线每次点击事件都覆盖对应槽位。这样地图始终只有三四个图层不会越积越多。另一个小技巧是给每个图层设置合理的名称和透明度。特别是全量电站点如果一次全画出来在区域数据密集的时候会盖住其他重要元素。我用灰色小点、transparency 0.6 显示全量电站用高亮的红点显示最近电站层次感一下就出来了。4.3 数据量过大时的两个优化先过滤再计算、必要时换reduceNearest如果全量发电站数据有几十万点直接在代码里powerPlants.map(...)跑一遍距离计算哪怕GEE的服务器性能很强一次响应也可能要好几秒。这个延迟在交互式工具里是难以接受的。我自己实测的经验是两条第一先过滤再计算永远是第一优先级。filterBounds能把候选点集缩小到目标点周边Buffer区域内这直接把参与计算的点数减少几个数量级。我在上面的实现里就是先targetPoint.buffer(radius)再做filterBounds效果明显。第二如果区域内的候选点还是有几十万个果断换reduceNearest。它走空间索引搜索不用对每个点都算距离。使用方式和方案一几乎同样简单var nearestFC tinyTargetFC.reduceNearest({ collection: powerPlants, key: distance, maxDistance: 200000 });两者唯一的差异是reduceNearest返回的是一个FeatureCollection你取.first()后属性的名称是distance语义一致。如果你的参考点不止一个比如想同时看三个选址点的最近电站reduceNearest的优势会体现得更明显——直接传一个多点集合进去批量返回结果不用写循环。这是方案一做不到的也是我后来更推荐它的原因。5. 常见问题与排查实录5.1 距离算出来全是0或者Null这个现象十有八九是几何类型或者坐标字段的问题。我先讲一个具体案例有人把CSV传上去字段名是lonlat但在构造FeatureCollection时忘记指定x和y参数GEE默认找longitudelatitude结果每个点的几何都是无效的。距离计算遇到无效几何自然返回Null或者0。排查方法很简单在代码里加一行print(powerPlants.first().geometry())看返回的是不是带坐标的Point。如果确认坐标没问题再看distance(targetPoint, 10)的第二个参数——maxError如果给成0在部分几何类型组合下会触发Too many dimensions这类奇怪的报错。我给10不会带来肉眼可见的误差。5.2 点击地图没有反应或者coords是undefinedmap.onClick回调一定是带一个坐标参数的但是如果你绑定的函数里先用了某些UI组件的值比如下拉框而这个值还没初始化就有可能在回调内部抛异常样子就像点击没反应。这时候按F12打开浏览器控制台看console有没有红色报错基本都能定位到。另一个更隐蔽的问题onClick的回调是在客户端执行的如果你在里面直接用了ee.FeatureCollection(...).sort(...)但没有.evaluate()或.getInfo()结果永远不会真正取回浏览器端UI也永远不会更新。我见过不少人卡在这一步——服务端计算逻辑完全正确但UI不刷新最后发现是缺少evaluate回调把值传回客户端。记住这条铁律凡是Server端的结果要更新UI都必须走evaluate。上面代码里的nearest.evaluate(function(val){...})就是这个作用。5.3 面板样式错乱或者组件挤在一起GEE的UI组件大部分问题都出在layout和style上。如果组件没有按预期方向排列检查两件事ui.Panel.Layout.flow(vertical)有没有写、父容器的style.width有没有足够宽。还有一个坑ui.Label里的内容太长了不会自动换行会撑破面板宽度。所以我习惯把长的字段值用substring包裹一下或者直接用ui.Label的多行样式whiteSpace: pre-wrap。5.4 发布成APP后别人无法访问数据GEE的APP默认情况下数据集必须对运行该APP的用户可见。如果你用的是公开数据集一般没有问题如果用了自己上传的私有资产需要去Asset页面把共享权限改成Anyone can view或者给特定的群组授权。这个授权是在资产盾牌图标里设置的不是在APP页面里。另外发布APP时选择Public链接别人才能用浏览器打开。很多人卡在这里——自己打开是好的别人打开全是空白或者报错一查几乎都是资产共享没开。5.5 常见问题速查表现象大概率原因解决办法距离全是0/Null几何无效或坐标系字段未匹配print检查geometry确认CSV坐标字段名点击地图无反应回调内异常或缺少evaluate打开浏览器Console查错确认结果用evaluate取回面板组件乱排漏写layout或父容器宽度不够检查ui.Panel.Layout.flow增大style.width最近电站不显示图层槽位被覆盖或图层名称冲突用map.layers().set统一管理槽位发布后他人无法访问私有资产未授权在Asset页面设置共享权限计算响应太慢候选点未先过滤filterBounds小Buffer数据过大时换reduceNearest6. 发布成APP和后续扩展方向6.1 把代码编辑器里的脚本变成真AppGEE的App发布入口在代码编辑器右上角的Apps按钮。选好名称后会生成一个链接打开就是纯地图模式不显示代码编辑器。发布之前建议做三件小事把地图初始中心点调整到你的业务区域、把面板里的测试字段清干净、确认所有用到的图层在加载时都有一个合理的名称。App发布后还可以嵌入到自己的网页里做内嵌分析工具GEE提供了对应的JavaScript库不过那是另一个话题了。这里分享一个体验细节发布后的App加载是全量加载页面如果你的代码在初始化阶段做了大数据量计算比如一开始就把全量电站全部画上地图首屏会明显卡顿几秒。解决方法是把全量图层的shown参数设成false或者干脆等用户第一次点击后再加载。首屏快用户第一印象就好。6.2 几个很自然的扩展方向这个模板改一改就能干很多事。比如新能源选址辅助加一层风速、光照或者土地利用图层用户点一个位置不仅显示最近发电站还同时显示周边适宜建新场站的比例信息。设施覆盖评估把发电站换成消防站、医院或者充电桩一样的代码逻辑输出最近站点名称和距离覆盖了一键查最近设施的场景。批量选址对比用reduceNearest批量传入多个目标点一次性输出每个点对应的最近电站清单可以直接拿去生成报告。时间维度扩展如果数据集带建设日期字段可以按年份过滤观察不同时期最近电站的变化这在追溯历史场景里非常有用。6.3 最后的两个小提醒回头整理这个项目的代码时我最想分享的两个小提醒第一写GEE的UI应用一定要分阶段开发先打通数据链路再搞界面否则调试成本成倍增长第二凡是涉及距离的展示一定要在代码里标注单位面板上显示22.3 km而不是22.3看的人不会误解你隔两个星期回来看代码也不会懵。这个工具本身不大骨架上也没多少行代码但它的使用场景比想象中广。说实话我在实际项目里用得最多的反而是那个清空按钮和下拉框的燃料筛选领导到现场要看哪个片区的情况点几下就出结果比打开ArcGIS再拖图层解释半天要高效太多。技术难吗不难。但它确实把一个平时要十分钟才能完成的活缩短到了十秒这就是地理工具面向业务落地时真正的价值。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑