简介:这是一份面向地质学或地球科学领域开发者与数据分析人员的 Python 应用资源,基于项目名与结构推断,它可能用于地质数据的处理、可视化或简单 Web 展示,适合正在学习 Python 在地学场景落地的读者参考。压缩包共 22 个文件,大小仅 14KB,核心为 14 个 Python 脚本,覆盖 Django 项目配置、视图、模型、迁移与测试等后端逻辑;同时包含 requirements.txt、Procfile 等部署配置,以及一个 HTML 模板、一个 CSS 样式文件和 SQLite 数据库文件,基本呈现了一个小型 Web 应用的完整目录结构,便于快速理解项目组织方式。目前已有 162 人浏览学习,属于轻量级入门示例。借助源码中的模块划分和依赖声明,读者可以了解 Django 基础工程搭建、数据库表结构定义、页面模板渲染及部署文件配置等关键知识点;若对地质数据展示感兴趣,还能基于这套框架扩展自己的可视化功能,尤其适合 Python 初学者作为首个 Django 项目拆解参考。 2019年夏天,我在大巴山南麓做野外踏勘。白天顶着太阳打产状、记描述、拍照片,晚上回驻地第一件事是打开笔记本电脑,把白天写在记录本上的地层岩性、构造产状、照片编号一项一项往Excel里誊。一份记录大概要花一个半到两个小时,稍不留神错行,后面画柱状图时要来回核对。那段时间我一直在想,既然手里已经有一台智能手机、有GPS、有大容量存储,为什么野外记录这件事还停在纸笔时代?回到成都以后,我决定用业余时间自己做一个真正顺手的野外记录工具,这就是geolog-app的由来。
这个项目最初的目标很简单:做一个在野外没有信号的环境下也能正常用的地质数据采集应用,能快速记录地质点信息、关联野外照片、画轨迹,数据不用联网就能存下来,回到室内一键导出成表格和GIS数据,不用再人工二次整理。如果你也是地质专业的学生、从事工程勘察的同行,或者对离线优先(local-first)类应用开发感兴趣的开发者,这篇文章会把我的技术选型思路、数据模型设计、核心实现细节和踩过的坑都摊开给讲一遍。
1. 项目背景与需求拆解:从野外记录本到“一键数字化”
1.1 传统野外地质调查工作流的痛点
传统野外地质记录的工作流大概是这样的:手拿地质锤和罗盘,到一个观察点,拿出记录本画信手剖面、记岩性描述、测产状,再用数码相机拍一组照片,照片编号和记录本里的点号要保持手动对应关系。回到室内之后,要面对一堆比较痛苦的事:一是野外记录是手写的,字迹、涂改、潦草程度各异,录入Excel时非常容易看错;二是照片和文字描述脱节,一张照片对应哪个地质点,经常要翻相机的时间戳去核对;三是后期想画地质图,要把点位坐标用GPS手簿导出来再转成shapefile,链路又多又长。
要承认的是,专业领域里已经有像“野外数据采集系统”这类商业软件,但大多数面向大单位采购,授权贵,部署重,而且交互设计往往还在用十几年前的桌面软件思维。对于个人科研、小团队踏勘、学生毕业设计这种场景,一套能直接打开网页就能用、不需要配置服务器、数据自己保存的轻量方案反而更接地气。
1.2 geolog-app 的定位与功能边界
分析了这些痛点后,我给自己定的产品边界很清晰:不要做成一个大而全的“地质信息系统”,只做“从野外采集到室内整理第一公里”这一件事。因此geolog-app第一版定位成浏览器端的渐进式Web应用(PWA),核心功能收敛为四个模块:
- 地质点快速记录:经纬度、海拔自动获取,岩性、时代、风化程度等字段用下拉选择加自定义补全,描述信息做成结构化表单。
- 照片关联:拍照后自动读取GPS和拍摄时间,与当前地质点绑定,支持批量挂载。
- 路线轨迹:在离线地图上实时显示位置,记录行动轨迹,标记已采集的地质点。
- 数据导出:一键生成CSV表格、GeoJSON文件和带缩略图的HTML报告,方便直接进QGIS或者OpenOffice等工具。
这个边界确定以后,后面所有技术选型都是围绕“轻量、离线、标准数据格式”这三个关键词来做的。
2. 整体架构与技术选型:为什么是 PWA + IndexedDB + Leaflet
2.1 技术栈清单与选型理由
先给出一份项目最初两星期内定下来的技术栈清单。这套组合在2020年前后已经非常成熟,放到今天依然不落伍,依赖很少,维护轻松:
- 前端框架:Vue 3全家桶。选它不是因为版本新,而是单文件组件天然适合做表单密集型页面,地质记录页的字段多、联动多,用响应式的UI描述起来比jQuery时代轻松太多。
- 构建工具:Vite。开发热更新快,打出来的包做静态托管很方便,USB拷到内网电脑也能跑。
- 地图组件:Leaflet 1.7 + OpenStreetMap离线瓦片。Leaflet的插件生态齐全(marker、polyline、坐标拾取),体积只有40KB左右,比OpenLayers轻很多,对手机浏览器够用。
- 本地数据库:IndexedDB,操作层用Dexie.js封装。Safari、Chrome、微信内置浏览器都支持,容量可以到几百MB甚至GB级,比localStorage的5MB限制靠谱得多。
- 定位:浏览器的Geolocation API + 原生GPS。在Android的Chrome里能调到高精度GPS;iOS对后台定位限制严格,所以我把定位设计成“点击获取”而不是持续监听。
- PWA壳:manifest + Service Worker用Workbox生成,用于缓存静态资源和离线地图瓦片。
为什么不用Electron做桌面客户端,也不用Flutter做原生App?原因很简单:地质工作者手头的电脑系统五花八门,Windows、macOS、Linux都有,而且野外用的可能是学校机房的老机器,装软件权限受限。一个PWA意味着只要这台机器有浏览器,就能直接打开使用;离线瓦片和数据库都存在浏览器里,换机器的时候导出一次数据即可迁移。
2.2 本地优先(Local-first)方案的优势
项目名字里有app,但它不需要走应用商店,这是刻意为之。野外环境最关键的要求是:网络是不可靠资源,硬件是有限的资源。传统App要登录账号、要同步服务器,这一点在深山老林里是一种负担。
本地优先架构把所有业务数据写入浏览器内置的IndexedDB,所有操作的服务端依赖降为零。这意味着:
- 打开应用不需要联网,加载本地缓存的壳资源即可。
- 写数据不经过网络层,响应速度就是本机磁盘写入速度,毫秒级。
- 数据权完全在用户手里,没有隐私泄露和上云合规的问题,非常适合科研数据管理。
- 断裂网络下依然可以完整工作,回到有网环境后再手动把数据备份文件导到其他设备。
作为补充,我做了一个“手动同步”机制,它不是服务器自动同步,而是将数据打包成一个JSON文件,可以存到私有网盘,也可以U盘拷贝。地质数据本质上是小数据量、高价值的文本加照片,一次野外采集最多几万个点,JSON完全撑得住,不需要后端数据库。
2.3 数据模型设计:让数据结构贴近地质调查规范
很多个人项目死在“没有认真设计数据表结构”这个坎上,后面加字段成了灾难。所以第一版写代码前,我先花了足足一个晚上用草稿纸画数据关系,最终确定了三个核心对象:geopoint(地质点)、photo(照片)、track(轨迹),外加四个辅助字典表:lithology(岩性)、strata(地层单位)、structure(构造类型)、weathering(风化程度)。
geopoint表是核心,设计上参考了区域地质调查中常用的记录项:
- id:自增主键或UUID,我用了UUID,方便离线合并。
- name:点号,例如D01、GPS-015,用户在野外可以自定义前缀。
- latitude/longitude:WGS84坐标系十进制度数,最多保留六位小数。
- elevation:海拔,单位米。浏览器定位接口带海拔,但精度差别大,所以允许离线后手动修改,比如从地形图上读取。
- rockType:岩性关键字,从字典表中选择,同时留freeText字段,允许补充像“含角砾凝灰岩”这种细化描述。
- attitude:产状对象,包含走向、倾向、倾角,前端做成三个数字输入框,后端存成JSON字符串。
- description:野外描述,这是最自由的字段,我做成多行文本,但提供相似模板,比如“灰—深灰色中厚层状灰岩,发育方解石脉,局部溶蚀现象明显”,用户可以一键插入再修改,省很多打字时间。
photo对象和geopoint之间是多对一关系,通过pointId关联。每张照片除了保存本地路径或Blob对象,还自动提取拍摄时间和Exif中的经纬度。这里有一个容易踩的坑:手机厂商对Exif的GPS写入策略差异很大,有的手机默认不写坐标,所以我在拍照模块里做了一个兜底——用拍照时Geolocation API拿到的实时位置覆盖Exif坐标,保证每张照片都有空间位置信息。
track对象按GeoJSON的LineString格式存储,保存为坐标点数组和对应时间戳,这样回放轨迹时可以做动态效果。
3. 核心功能实现要点:记录、关联、导出一条线
3.1 地质点记录表单的交互设计
地质记录这个场景有个和普通表单不一样的地方:用户的手指可能刚碰过岩石粉末,可能戴着薄手套,也可能正在雨里打着伞。所以表单交互必须做到“一键直达”和“大按钮优先”。
表单首屏默认只有点号、经纬度、海拔和岩性四个字段。点击“获取当前位置”按钮,定位成功后会同时填充经纬度和海拔,字段右上角会显示定位精度,比如±5m。精度较差时我用橙色三角图标提示,用户可以稍后重新定位。新增地质点时默认从字典表给出下拉选项,比如岩性下拉列表的选项包括:灰岩、白云岩、砂岩、页岩、玄武岩、花岗岩、片岩、片麻岩、千枚岩、板岩、大理岩、混合岩、砾岩、泥岩、凝灰岩、安山岩、流纹岩等,每个字典项还可以维护别名,比如“灰岩”的别名有“石灰岩”。
描述字段引入“常用模板”是第二版才加的功能,但实测下来效率提升最大。野外描述其实高度套路化,比如沉积岩的套路是“颜色+结构+构造+物质成分+化石+风化特征+厚度+接触关系”,我为每类岩石准备了三到五套模板,点击模板之后自动插入当前点描述,只需要改数字和局部特征词,记录时间从五分钟缩短到一分钟。
3.2 照片与GPS坐标的自动关联
照片管理是地质数据里最容易乱套的部分。geolog-app里我实现了两种照片采集方式。一种是在app内直接调用摄像头拍照(使用HTML的capture="environment"属性),拍照后照片以Blob形式存入IndexedDB,同时触发定位API获取当前位置。第二种是从相册选择已有照片,这个时候会尝试读取图片的Exif信息,用exifr这个库解析GPS坐标和拍摄时间,如果Exif里没有坐标,就弹一个手动匹配地理位置的对话框,允许用户选择当前地质点作为照片位置。
照片存入时还有一个必须在代码里处理的细节是图片压缩。不压缩的话,一张1200万像素的手机照片体积大概在2MB到4MB之间,一天拍两百张就是600MB,IndexedDB虽然能存,但手机存储空间会告急,而且导出的HTML报告会变成一个几百MB的文件。我引入了一个canvas压缩管线:最大边压缩到2048像素,JPEG质量设为0.72,实测单张照片大约300KB,肉眼在屏幕上观察野外细节完全够用。“原图无损”选项保留着但默认关闭,适合需要看岩石显微结构的场景。
3.3 离线地图、轨迹标注与数据导出
虽然是“离线应用”,但地图的底图始终需要素材。我的方案是:在进入野外之前,在Web端选择一个兴趣区域(可以画多边形),应用根据区域范围自动计算出需要的瓦片金字塔层级(比如第8级到第17级),然后批量下载OpenStreetMap瓦片并存入IndexedDB缓存。下载完成后,这部分瓦片就永久保存在设备里,野外即便完全断网也能正常平移动图。
轨迹功能利用了浏览器的watchPosition方法,每五秒记录一次坐标,并将坐标点实时绘制到Leaflet的polyline上。采集地质点时,应用自动生成一个带点号的圆形Marker,点击Marker可以跳转到该点详情。这里我特意做了防误触设计:Marker半径做成了地图像素上的12px,比Leaflet默认大,野外手指点起来不容易弹错,同时用不同颜色区分“已记录描述”和“只打了点”两类标记。
数据导出模块是我写的最早的模块,因为我觉得一个数据采集工具要是导不出标准格式,就毫无意义。目前支持三种导出格式:
- CSV表格:列出所有地质点,包含坐标、海拔、岩性、产状、描述,贴合通用电子表格。
- GeoJSON文件:可以直接拖进QGIS,作为矢量图层继续做花纹填充和出图。
- 自带资料的HTML报告:打包所有照片缩略图和野外描述,按照点号排序,适合直接微信发送给合作者快速浏览。
实现导出时需要注意一个坑:浏览器下载两个文件以上,某些浏览器会询问多次存储权限,体验不好。我的做法是将整个导出活动做成两步:先导出原始数据(CSV+GeoJSON打包为ZIP),再导出图文报告(单独的HTML),用JSZip来打包ZIP文件。
4. 踩坑实录与排查技巧
4.1 定位精度的坑:坐标系偏移与冷启动
这个项目早期在调试定位时经常出现“明明站在同一块露头上,记录的点位却漂到坡下去了”的情况,排查之后发现有两个诱因。
第一个是坐标系问题。浏览器Geolocation API原生返回的是WGS84坐标,而国内很多在线地图底图(包括一些离线瓦片源)使用的是加偏坐标系GCJ-02。如果你把WGS84坐标直接叠加到加偏底图上,偏移量少则几十米,多则两百米,在1:50000地质图上看起来就像点错位碳酸盐岩界线上。我的做法是不用在线地图底图,改用OpenStreetMap标准WGS84瓦片,同时在应用里给了一个坐标转换小工具,方便用户在底图之间切换时做转换。设计上我建议所有原生数据都以WGS84存储,显示层再根据底图设置决定是否转换,这样数据本身永远是干净的。至于“偏移”背后的更多细节,属于地图合规范畴,这里不展开,只提醒一句:野外记录务必以GPS原生坐标为基准,不要基于加偏底图反推点位。
第二个是冷启动问题。GPS模块在刚打开浏览器时,如果长时间待在室内再走到室外,定位结果往往直接取用基站或Wi-Fi网络定位数据,误差可能达到500米以上。我的规避措施是在“获取当前位置”按钮上做了连续三次采样,如果相邻两次定位结果相差超过30米,就显示提示“位置不稳定,请等待GPS锁定”,并且十秒内自动再补采一次。实际在山里使用下来,一般等待二三十秒,精度能稳定在5米以内。
4.2 存储限制与照片体积问题
IndexedDB理论容量很大,但不同浏览器的实际配额策略很不一样。我实测过Chrome在Android上能稳定用掉几个GB,iOS Safari通常在500MB左右会提示站点存储空间不足,而桌面端Firefox的配额策略相对保守。为了解决这个问题,我在“设置”页面放了存储统计面板,显示当前已用空间、照片张数和总容量上限,并且提供“清理未关联照片”按钮,一键删除没有被任何地质点引用的孤儿图片。这个功能看似简单,野外用几天之后清理出几百MB是很常见的事情。
再说照片方向问题:用手机竖屏拍出来的照片,很多时候在Web页面里显示会被旋转90度。这个坑源自Exif里的Orientation字段。我用canvas做压缩管线时把Orientation字段也一并解析并应用到画布,导出时统一输出成标准方向的JPEG。如果这一步漏掉,报告链接一打开,一半照片是躺着的,就是断送项目口碑的细节。
4.3 常见问题排查速查表
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 点击“获取当前位置”无反应 | 未授予定位权限,或浏览器阻止了非HTTPS页面的Geolocation | 确认通过localhost或HTTPS访问;在浏览器设置里手动放行定位权限 |
| 坐标为0,0 | 定位失败偶发错误,返回无效值 | 判断isNaN或者值等于0时重新采样,后台自动重试三次 |
| 地图瓦片加载不出来 | 选择的区域层级过大,下载中途切后台 | 分多次下载,每次区域控制在一度范围内;检查IndexedDB是否有缓存 |
| 导出GeoJSON在QGIS打开无图层 | QGIS需要指定CRS | 在GeoJSON文件里显式写入EPSG:4326的CRS属性,QGIS就能正确识别 |
| 导入照片不显示 | 浏览器读取本地文件到Blob时未持久化 | 确认照片Blob已经存入IndexedDB;不要在会话结束后仅引用临时object URL |
| 应用更新后旧数据不见了 | Service Worker缓存了旧版shell | 更新后强制刷新并清一次缓存,把版本号放到manifest配置里管理 |
4.4 一个值得留意的多设备合并经验
因为我一开始就考虑到野外的数据最终要汇总到同一台电脑上,所以每条数据都带UUID和updatedAt字段。回到室内以后,有两台机器分别记录了不同路线,就可以用“导出全部数据”生成两个JSON文件,然后通过“导入备份”功能合并。合并策略并不复杂:以updatedAt为准,谁的最新就用谁的,同时保留同一点号不同描述的历史版本。
实测下来有一个小坑:两天的数据库里点号都是从D001开始编号,合并后会出现重复点号,导致表格里看起来像有两个D001。我后来做了一个“自动重编号”选项,导入时可以按合并顺序统一重排点号,并把照片文件名里的点号一并替换。这个功能在项目交数据给负责人的时候帮了大忙,不需要再用Excel函数处理一遍重复值。
在我把这些功能真正带到下一轮野外项目里用掉一个完整季度之后,最大的体会是:工具类项目最核心的成本不在写第一版功能,而在持续打磨那些“舒服的小细节”。一个自动补全的描述模板、一次点按就能完成的照片关联、一键导出的标准化数据,这些看起来不那么“核心技术”的东西,恰好是让一个工具从“能用”变成“好用”的分水岭。现在geolog-app已经是我个人野外踏勘的标准配置,我也把项目开源出来,如果你也有类似的地质记录或者其他领域的野外数据采集需求,完全可以把它当作一个参考样板,替换掉地质字典表和导出模板,几分钟就能改成植物调查或者考古点记录工具。最后再分享一个小技巧:app里我设计了一个“草稿”概念,地质点在没有点“完成”之前不会出现在轨迹图上,这样遇到临时拿不准、需要回头补充的描述也可以先存半截记录,过程中真的很少丢数据了。
本文还有配套的精品资源,点击获取