简介这份资源是基于GIS地理信息系统的绘图程序源代码面向GIS开发学习者与地理信息相关专业学生目标是解决地理数据创建、编辑与可视化展示问题。压缩包共55个文件主要以20个h头文件、18个cpp源文件组成还包含bmp、ico、rc等资源与工程配置类文件工程结构清晰适合用于学习MFC框架下的GIS绘图开发。资源仅183KB代码量精简便于逐行阅读和二次开发覆盖了点、线、多边形、圆等基础图形的鼠标绘制以及颜色填充、mif与shp数据格式处理、地图缩放等关键知识点。对于希望从零掌握GIS编程或完成相关课程设计的人员是一份很有参考价值的入门源码。该资源已有432人浏览学习感兴趣的开发者可下载参考。1. 先说清楚GIS 绘图程序源码解决的是什么问题GIS 地理信息系统的绘图程序是近两年业务系统里被点名要得最多的一个模块外业调查要现场描边界资产盘点要把设备摆到地图上工程验收要在图上圈整改区域。过去靠纸质图纸描完再回办公室录坐标一个点录错整张图返工现在常见的做法是把画图功能直接嵌进内网系统鼠标点几下就能产出标准空间数据。这个标题对应的源码就是把底图加载、图层组织、点线面绘制、坐标换算、捕捉闭合、数据导出这几块全部串起来的一套程序。我按网页端内嵌绘图工具这个最常见的交付形态展开适合要给业务系统加制图能力、或者正在为团队做内部 GIS 工具的开发者。2. 技术选型先锁坐标系、数据结构与渲染库2.1 为什么是 Web 交互渲染加几何计算这套组合绘图程序的核心体验不在“画”这个动作本身而在交互反馈鼠标一落点图形立刻出现在底图上双击结束多边形自动收口鼠标靠近已有顶点光标被“吸”过去。这些交互对事件响应速度和图层刷新频率要求很高。浏览器端成熟的交互渲染库把这些能力做成了内置模块包括绘制、修改、吸附和坐标变换开发时只需要关心业务逻辑不需要自己维护一套鼠标状态机和渲染循环。桌面端用 Python 路线也能做常见组合是 GUI 框架加地理处理库适合离线批量处理、对文件格式兼容性要求高的场景。但把它做成多人使用的交互绘图工具事件循环、图层刷新、坐标变换都要手写分发时还依赖 Python 环境维护成本明显偏高。两条线我给你放在一起对比方案主线代表组件交互绘图能力部署与维护适合场景桌面 Python桌面 GUI 框架 地理数据处理库需要手写大量事件与图层刷新逻辑依赖运行环境分发给使用者较繁琐离线批量处理、内业整理Web 浏览器开源交互渲染库 几何计算库官方内置绘制、捕捉、编辑交互浏览器打开即用代码随前端工程发布在线采集、协同作业、业务系统集成我一般会直接选 Web 这条线把精力放在坐标系、数据组织和业务交互上。标题里说的“绘图程序源码”在大多数实际项目里交付的就是这样一个浏览器端工具。2.2 坐标系与投影EPSG:4326 和 EPSG:3857 怎么选绘图程序里最容易混的是四个概念屏幕像素、视图坐标、地理坐标、存储坐标。屏幕像素是鼠标事件里拿到的数字视图坐标是地图 View 使用的投影坐标系浏览器端底图绝大多数采用 Web 墨卡托投影也就是 EPSG:3857地理坐标是经纬度即 EPSG:4326存储坐标是真正写进数据库或文件的坐标业务上为了通用性通常也选 4326。坐标系EPSG 编号单位特点典型场景WGS84 经纬度4326度全球统一的经纬度表达数据存储、文件交换、GPS 采集Web 墨卡托3857米全球瓦片统一、平面坐标直观底图渲染、浏览器端视图坐标系推荐的链路很简单底图和视图坐标系用 3857数据存储和交换统一用 4326。GPS 采集到的经纬度数据进入绘制图层前要先转成 3857导出数据或写入数据库前再转回 4326。这条链路定下来后续所有代码都不会打架。如果哪一步漏了转换图形要么画到海里要么导出后用其他 GIS 软件打开整体偏移。提示视图坐标系和存储坐标系分开设是后续所有代码不打架的前提。前端开发最常犯的错就是把这两个当成一回事。2.3 数据结构与图层组织用 GeoJSON 组织要素图层按职责拆绘制程序里的数据组织我推荐直接用 GeoJSON。它本质是 JSON前端不用做任何解析序列化、网络传输、调试都非常方便要素和属性天然绑定在一个对象里画完一个地块地块名称、类型、面积都能直接挂在同一个要素上。Shapefile、KML、GeoJSON 互转的工具链也很成熟服务端空间数据库大多也支持直接入库。绘图程序要处理的几何类型核心就三种几何类型典型用途绘图方式Point设备点位、标记、采样点单击落点LineString道路、管线、边界线逐点点击双击结束Polygon地块、区域、整改范围逐点点击双击闭合图层组织上我习惯分成三层底图图层只做展示、不可编辑绘制图层放用户新画的要素、允许增删改辅助图层用来做高亮、测量结果和临时预览。绘制图层内部如果业务复杂可以继续按几何类型拆成点、线、面三个数据源这样每个图层的样式、选中逻辑、查询逻辑互相独立不会互相干扰。常见的反面教材是把点线面全塞进同一个数据源样式一混后期做选中高亮和属性编辑时非常痛苦。3. 最小可运行工程从零搭出能画点线面的 GIS 界面3.1 用 Vite 初始化工程并安装依赖先搭工程骨架。我习惯用 Vite 的 vanilla 模板因为绘制程序的主体代码就是地图初始化加几个事件监听不需要引入前端框架增加干扰。依赖只需要两个一个是交互渲染库负责地图、图层和绘制交互另一个是几何计算库后面做几何简化、面积计算和空间判断时用。npm create vitelatest gis-draw -- --template vanilla cd gis-draw npm install ol turf这里的ol是浏览器端地图渲染与交互的核心库前面说的绘制、捕捉、修改交互都由它提供turf是几何计算工具库第 5 章讲大数据量优化时会用到它的简化功能。装最新稳定版即可。如果你只是临时验证效果也可以不搭工程直接通过官方 CDN 引入脚本但那样后面做模块化封装会吃力建议正式做还是走构建工具。3.2 加载底图并建立空的绘制图层初始化页面和地图。底图先用开源街道地图瓦片来做调试生产环境换成符合 XYZ 规范的内部瓦片或商业瓦片即可。绘制图层是一个空的矢量图层后续所有画出来的要素都进入这个图层。!doctype html html head meta charsetutf-8 / titleGIS 绘图程序最小工程/title style html, body { margin: 0; height: 100%; } #map { width: 100%; height: 100%; } #coord { position: fixed; bottom: 12px; left: 12px; z-index: 999; } /style /head body div idmap/div div idcoord经度 --, 纬度 --/div script typemodule src/main.js/script /body /htmlimport Map from ol/Map.js; import View from ol/View.js; import TileLayer from ol/layer/Tile.js; import OSM from ol/source/OSM.js; import VectorLayer from ol/layer/Vector.js; import VectorSource from ol/source/Vector.js; import { fromLonLat } from ol/proj.js; // 底图本地调试用开源街道地图生产环境替换为内部瓦片源 const baseLayer new TileLayer({ source: new OSM(), }); // 绘制数据源初始为空所有画出来的要素都进这里 const drawSource new VectorSource(); const drawLayer new VectorLayer({ source: drawSource, }); const map new Map({ target: map, layers: [baseLayer, drawLayer], view: new View({ center: fromLonLat([116.4, 39.9]), // 示例坐标按业务区域替换 zoom: 12, }), });这段代码里要注意两处第一View 的center不能直接写经纬度数组必须用fromLonLat转成当前投影坐标否则视图中心会落到完全错误的位置第二底图图层和绘制图层是分开的两个图层绘制图层有自己的数据源这为后面做编辑、保存和样式控制留下了独立空间。3.3 点、线、面绘制模式的切换逻辑地图能显示之后加入绘制交互。核心是一个创建和销毁绘制交互的状态切换逻辑点、线、面三种模式都通过同一个函数进入。import Draw from ol/interaction/Draw.js; let drawInteraction null; function startDraw(type) { if (drawInteraction) { map.removeInteraction(drawInteraction); } drawInteraction new Draw({ source: drawSource, type, // Point | LineString | Polygon snap: true, // 开启顶点/边捕捉细节见第4章 freehand: false, // 关闭自由手绘逐点点击绘制 }); map.addInteraction(drawInteraction); } function stopDraw() { if (drawInteraction) { map.removeInteraction(drawInteraction); drawInteraction null; } }type决定绘制交互产生的几何类型LineString 和 Polygon 都是逐点点击、双击结束的交互模式snap: true会在存在其他要素时自动吸附到它们的顶点和边上freehand: false让用户一笔一笔点出几何形状而不是按住鼠标拖出一串密集点。工具栏按钮直接调用startDraw(Point)、startDraw(LineString)、startDraw(Polygon)就完成了模式切换。这里有个高频坑必须强调从一种绘制模式切到另一种时一定要先removeInteraction移除旧交互。如果只是隐藏工具栏按钮旧绘制交互仍在监听地图事件会出现“我在画线屏幕上也同时在画面”的诡异现象。4. 核心模块实现坐标换算、对象捕捉与数据导出4.1 鼠标坐标换算把屏幕像素变成规范的经纬度绘图程序里鼠标给的是屏幕像素要变成底图上正确的坐标才能落点落点之后要保存还得把视图坐标转成经纬度。这两个方向都要处理。下面这段代码做了两件事状态栏实时显示鼠标所在经纬度绘制结束时把要素坐标转成 4326 输出。import { toLonLat } from ol/proj.js; // 鼠标移动时实时显示经纬度 map.on(pointermove, (evt) { const viewCoord map.getCoordinateFromPixel(evt.pixel); // viewCoord 是当前 View 投影坐标系下的坐标默认 EPSG:3857 const lonLat toLonLat(viewCoord); document.getElementById(coord).textContent 经度 ${lonLat[0].toFixed(6)}, 纬度 ${lonLat[1].toFixed(6)}; }); // 绘制结束时把几何坐标转换并输出 drawInteraction.on(drawend, (evt) { const geometry evt.feature.getGeometry(); // 先 clone 再 transform避免把绘制图层里的要素坐标原地改成 4326 const coords geometry.clone().transform(EPSG:3857, EPSG:4326).getCoordinates(); console.log(JSON.stringify(coords)); });这段代码里最值得记住的是clone()这一步。transform会原地修改几何对象的坐标数组如果你直接对geometry调用 transform绘制图层里这个要素的坐标就变成了 4326和底图的 3857 坐标系不一致后续再画新要素时捕捉、量距全部错乱。这是非常典型的血泪经验先把几何克隆一份再转换原要素在图层里保持不动。4.2 开启对象捕捉绘制时自动吸到已有顶点和边上捕捉是绘图程序“好用”和“能用”的分水岭。画一栋建筑的边界时下一笔能自动吸附到上一笔的顶点图形才不会出现肉眼可见的缝隙。浏览器端交互渲染库的 Draw 交互内置了基础捕捉能力打开snap: true就能用。但内置捕捉的容差是固定的如果想控制吸附距离或吸附到其他静态图层需要单独创建捕捉交互。import Snap from ol/interaction/Snap.js; const snap new Snap({ source: drawSource, // 像素容差鼠标距离目标顶点或边多少像素内触发吸附 pixelTolerance: 10, }); map.addInteraction(snap);source指定要吸附的要素集合一般就是绘制图层的数据源pixelTolerance是触发吸附的像素范围10 像素是比较自然的操作手感太小了很难精准命中太大了会感觉鼠标被“拽”着走。需要注意注册顺序先添加 Snap 交互再添加 Draw 交互这样鼠标移动事件先经过捕捉修正绘制的落点才是吸附后的坐标。注意如果底图上有其他需要吸附的要素比如已有的道路中心线要把它们的几何临时加入 Snap 的 source否则绘制时只能吸附到当前图层自己画出来的要素吸附范围就窄了。4.3 导出与导入 GeoJSON坐标系转换与文件下载绘制完成的要素要能落盘、能交出去导出为 GeoJSON 是最通用的做法。导出时最关键的参数是投影设置数据用 4326 输出保证其他 GIS 软件打开时位置正确。import GeoJSON from ol/format/GeoJSON.js; function exportGeoJSON() { const features drawSource.getFeatures(); if (features.length 0) return; // dataProjection 指定输出文件的坐标系featureProjection 指定要素当前的坐标系 const geojson new GeoJSON().writeFeaturesObject(features, { dataProjection: EPSG:4326, featureProjection: map.getView().getProjection(), }); const blob new Blob([JSON.stringify(geojson, null, 2)], { type: application/geojson, }); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download draw-result.geojson; a.click(); URL.revokeObjectURL(url); }writeFeaturesObject的两个投影参数缺一不可featureProjection 告诉序列化器要素当前在什么坐标系dataProjection 决定输出文件用什么坐标系。很多项目在这里只写了 dataProjection 没写 featureProjection序列化器就会错误地按默认规则处理导出文件在 GIS 软件里整体偏移。反向导入思路完全一致调用readFeatures时同样指定dataProjection: EPSG:4326和featureProjection: EPSG:3857浏览器端就能自动把经纬度坐标转回视图坐标然后clear再addFeatures重新加载进绘制图层。5. 避坑清单坐标系、性能与交互的翻车现场5.1 坐标系错乱图形画着画着就跳走了现象画到一半的图形在屏幕上突然移位或者导出后用其他 GIS 打开要素整体偏移到完全不相干的位置。原因鼠标交互拿到的getCoordinateFromPixel返回值是当前视图坐标系3857的米制坐标有人直接把它当成经纬度写入存储字段或者导出时漏了dataProjection参数坐标系信息缺失导致下游解析错误。这类问题隐蔽在数据链路里界面上一开始看不出异样等到数据交换时才暴露。解决核心原则是“界面用 3857存储用 4326”所有跨边界的地方显式转换。如果需要单独转换某个坐标点直接用转换函数import { transform } from ol/proj.js; // 把视图坐标转成经纬度注意 transform 会原地修改数组 const storeCoord transform(viewCoord, EPSG:3857, EPSG:4326);transform是原地操作返回的数组和传入的viewCoord是同一个引用如果你后面还要用原始坐标先slice()复制一份再转换。5.2 面要素不闭合双击结束但多边形少了一条边现象画多边形时双击结束图形看起来闭合了但导出后某些 GIS 软件报几何无效或者面积计算结果异常。原因双击结束时鼠标的实际位置离起点还差几个像素绘制交互自带的闭合只在距离小于捕捉容差时生效。容差没命中几何对象的首尾坐标就不相等多边形在拓扑上是个开放的环。解决两个手段叠加使用。一是绘制时开启snap: true并把捕捉容差调大一点二是在drawend回调里做兜底闭合检查首尾坐标不相等就手动补上闭合点drawInteraction.on(drawend, (evt) { const geom evt.feature.getGeometry(); if (geom.getType() Polygon) { const coords geom.getCoordinates()[0]; const first coords[0]; const last coords[coords.length - 1]; if (first[0] ! last[0] || first[1] ! last[1]) { coords.push(first.slice()); geom.setCoordinates([coords]); } } });这里first.slice()是为了复制一个新数组不要直接把first引用 push 进去否则后面编辑几何时会因为共享引用产生连锁修改。这段兜底逻辑在大多数据量场景下都能稳定生效是面绘制可靠性的最后一道保险。5.3 数据量一大就卡矢量图层渲染与几何简化现象绘制图层里加载了几千个要素后地图拖动和缩放明显掉帧绘制过程中每次鼠标移动都像在拖一扇沉重的门。原因默认的 Canvas 渲染器在每次视图变化时会把图层内所有要素重新投影、重新绘制一遍。要素多、顶点密重绘开销就指数上升。几何越复杂投影计算越耗时这是绘制程序最容易遇到的性能瓶颈。解决三个层级依次处理。第一层是几何简化用几何计算库把顶点密度降下来import { simplify } from turf; // 对单个 Feature 进行几何简化 const simplified simplify(featureGeoJson, { tolerance: 2, // 简化容差单位与几何坐标一致3857 下是米 highQuality: true, // 高质量算法速度慢但保留形状更好 });第二层是把大量静态要素移出绘制图层已经不需要编辑的要素合并成静态数据要么切到 WebGL 渲染图层要么直接发布成底图瓦片让绘制图层只保留用户当前正在编辑的小批量数据。第三层是控制交互目标绘制过程中的实时计算尽量只在当前正在绘制的要素上做不要遍历整个图层。5.4 导出大文件内存爆掉现象要素量超过一万时点击导出页面直接卡死浏览器报内存不足或者导出的 GeoJSON 文件有几十兆下载都费劲。原因writeFeaturesObject会先把所有要素序列化成一个大 JS 对象接着JSON.stringify又生成一份完整字符串这两份数据同时存在内存里。要素多、坐标点密时内存占用轻松翻倍浏览器扛不住就崩了。解决要素量上万前端就不该再做整包导出。常见做法是前端只把要素的坐标和属性整理成数组通过 POST 请求发给服务端由后端生成 GeoJSON 或 Shapefile 文件流返回前端只负责显示下载链接。如果必须纯前端处理就用压缩库把 GeoJSON 文本压缩后再下载能明显降低传输和落盘压力。6. 进阶把绘图能力封装成可复用的 DrawManager6.1 DrawManager 的接口设计与核心代码画图功能写到最后最怕的是所有状态都散落在页面各处。我习惯把绘制交互封装成一个独立的类名字随意比如 DrawManager。这样不管页面里有多少按钮、多少图层绘图模块的入口和出口都只有几个方法二次开发时换页面、换框架都不需要重写绘制逻辑。class DrawManager { constructor(map, source) { this.map map; this.source source; this.draw null; this.history []; } start(type, options {}) { this.stop(); this.draw new Draw({ source: this.source, type, snap: options.snap ! false, freehand: options.freehand || false, }); this.map.addInteraction(this.draw); } stop() { if (this.draw) { this.map.removeInteraction(this.draw); this.draw null; } } clear() { this.source.clear(); this.history []; } }把绘制交互的创建、切换、销毁全部收敛到一个类里页面只负责调start(Polygon)、stop()和clear()不需要关心内部的状态管理。history数组是为后续的撤销重做预留的位置每次drawend后把要素 id 推进去撤销时从 source 里移除对应要素即可。6.2 参数说明与二次开发中的使用习惯方法参数含义典型调用starttype 指定几何类型options 控制捕捉与手绘模式manager.start(Polygon, { snap: true, freehand: false })stop无参数彻底移除当前绘制交互manager.stop()clear清空当前绘制图层并重置历史记录manager.clear()封装完这个类绘图程序的主体代码就已经和具体页面解耦了。后续如果要加量测工具、面积统计、属性弹窗都是在drawend事件回调里挂新的逻辑不用再动绘制核心。我现在做这类功能时习惯先把视图坐标系、存储坐标系和数据字典定死再写任何一行绘图代码。坐标系定了、图层边界清了剩下的绘制交互在哪个框架里都能做出来。画图功能本身不难返工基本都返在投影和数据组织上这是踩过几次坑之后最想提醒自己的一条。希望帮到你。本文还有配套的精品资源点击获取