GPR 数据到底长什么样:A-scan、B-scan 与采样定理
探地雷达往地下打一束高频电磁脉冲,遇到材料变了的地方,一部分反射回来,剩下的继续往下走。仪器只记录两个数:什么时候回来,回来有多强。所有 GPR 数据说到底就是这个。
反射和深度反射强弱看界面两边材料差多少:
$$R = \frac{\sqrt{\varepsilon_1}-\sqrt{\varepsilon_2}}{\sqrt{\varepsilon_1}+\sqrt{\varepsilon_2}}$$
混凝土介电常数大概 6~9,空气接近 1。衬砌背后出现空洞时,两边差得大,反射就强。空洞、脱空能被探到,原因就在这里。
深度从回波时间换算:
$$d = \frac{v \cdot t}{2}, \qquad v = \frac{c}{\sqrt{\varepsilon_r}}$$
$t$ 是电磁波下去再上来的双程时间,所以除以 2。光速 $c=0.3,\text{m/ns}$,混凝土里波速大概 $0.10\sim0.12,\text{m/ns}$。回波时间对应深度,回波强弱对应界面差异——这两句话是后面所有算法的基础。
A ...
零线刷新幂等去重 缓存数组逐元素比较
现象零线对齐初始化那阵,发现磁盘被频繁读、初始化很慢。profile 了一下,看到 reloadBuffer 被反复触发了很多次。
原因初始化时有好几个事件源(不同组件的状态变更)都会触发 reloadBuffer——也就是重新从磁盘读雷达数据再刷新。这些事件短时间里密集到达,虽然最后零线的状态其实没变,但每次都老老实实重读一遍磁盘,做了大量重复 IO。
去重要让刷新幂等,说白了就是相同的输入只处理一次。把上一次的 zeroPositions 数组缓存下来,新事件来了先比一比新旧:
fun onZeroLineUpdate(newPositions: DoubleArray) { if (newPositions contentEquals lastPositions) return // 没变,跳过 lastPositions = newPositions.copyOf() reloadBuffer()}
为什么不算哈希第一反应可能是算个哈希比一下,快。但哈希会碰撞:两个内容不同的数组哈希恰好一样,就会被当成”没变”漏掉刷新,雷达图就错位了 ...
雷达采样直方图从 O(N×M) 优化到 O(N) 单遍桶计数
背景雷达图渲染前要统计采样值的分布——算百分位、做增益和对比度映射,这步一般叫”采样直方图”。打开一个多文件的测线时,每打开一个文件都得算一遍。
原来的 O(N×M)最早的做法是”对每个待统计的输出桶,把所有采样点扫一遍,数有多少落进这个桶”。N 个采样点 × M 个桶,复杂度 O(N×M)。单文件几百万采样点的时候,光这一步就把打开速度拖慢一大截,多文件叠在一起更明显。
改成 O(N) 单遍桶计数其实直方图没必要每个桶都扫一遍数据。一趟遍历,把每个采样点直接扔进它对应的桶就行了:
fun histogram(samples: IntArray, min: Int, max: Int, bucketCount: Int): IntArray { val buckets = IntArray(bucketCount) val range = (max - min).coerceAtLeast(1) for (v in samples) { val idx = ((v - min).toLong() * bucketCount / ran ...
Kotlin 协程生命周期 SupervisorJob 与 Mutex 超时保护
SupervisorJob默认情况下,协程作用域用的是 Job(),父子关系就像一根绳上的蚂蚱——任意一个子协程抛异常,整个作用域(包括其他正常跑着的兄弟协程)都会被取消。对于”加载多条测线、其中一条失败不该拖累其他”的场景,这就太脆了。
换成 SupervisorJob(),子协程的失败不会往上传播,其他兄弟照常跑:
class RadarViewModel { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)}
@PreDestroy协程要是不主动取消,它会一直挂着,引用着 ViewModel / UI,造成内存泄漏,甚至组件都销毁了还跑回来刷新一个已经失效的 UI。把 scope 的取消绑到生命周期上:
@PreDestroyfun onDestroy() { scope.cancel() // 取消所有子协程}
Mutex 加超时共有 / 复合病害的计算可能短时间里被触发好几次(连续编辑),不加锁的话,两个并 ...
JavaFX Canvas 踩 GPU 纹理上限 视口裁剪方案
现象有条超长的层位线(20000+ 道)在雷达图上只显示了一截,甚至整段空白;短一点的层位线倒是正常。
原因原来的渲染是把整段层位线一口气画到一个 Canvas 上,Canvas 的宽度就等于这段的道数。一条 20000 道的层位线,Canvas 宽度起步就是 20000 像素,缩放之后还更高。
而 GPU 纹理是有尺寸上限的,一般在 16384 ~ 32768 像素之间,看显卡。Canvas 宽度一旦超过这个上限,渲染就直接失败——而且是静默失败,表现就是空白或者只画了一截,啥报错都没有,排查起来很费劲。
视口裁剪办法就是只画当前能看见的那一截,整段别都画。Canvas 宽度始终差不多等于视口宽度(屏幕上能看到的范围),内容跟着滚动、缩放重绘:
根据当前的滚动位置和缩放,算出视口覆盖的道号区间 [from, to]。
只把这个区间里的层位线点画到 Canvas 上。
Canvas 宽度固定成视口宽度,永远不会越过 GPU 的纹理上限。
用户一滚动或者缩放,就重新算区间、重绘。
附带的优化视口裁剪顺带把性能问题也解决了:以前不管你看哪,都在画 20000 个点,现在只画视口里那几百 ...
SQLite 批量更新 CASE WHEN 与 999 变量上限
痛点病害的”共有 / 复合”状态算完得持久化。几百上千条病害,最开始是逐条 UPDATE,两个毛病:一是慢,每次 UPDATE 都是一次磁盘事务;二是没有整体事务保护,中途一旦失败,就留下一堆半新半旧的数据。
CASE WHEN 批量更新把 N 条逐条 UPDATE 合成一条,用 CASE WHEN 给每行不同的值:
UPDATE defectSET compound_level = CASE id WHEN 101 THEN 'AA' WHEN 102 THEN 'A1' WHEN 103 THEN 'B' END, is_compound = CASE id WHEN 101 THEN 1 WHEN 102 THEN 1 WHEN 103 THEN 0 ENDWHERE id IN (101, 102, 103)
一趟往返就更新了多行多列,磁盘事务次数从 N 次降到 1 次。外面再用 TransactionTe ...
用事件总线解耦模块 零线变更的级联重算
问题用户调一下”零线”,后面会牵一发动全身:受影响的文件得重算厚度不足段、层位段平均深度、病害深度,层位识别和病害标注那边的雷达图、列表也都得跟着刷新。
要是让零线模块直接去调各模块的刷新方法,耦合就焊死了——零线模块得记着谁要刷新、调谁的哪个方法,以后多加个模块还得回来改零线的代码。
事件总线加一个轻量的事件总线,零线模块只管”发事件”,谁在意谁自己”订阅”:
// 发布方:零线模块,不关心谁来处理eventBus.publish(ZeroLineChangedEvent(affectedFileIds))// 订阅方:层位识别模块,自己决定怎么响应@Subscribefun onZeroLineChanged(e: ZeroLineChangedEvent) = scope.launch { recalcLayerSegments(e.affectedFileIds) refreshChart()}
好处其实就是几件事搅一块儿:发布方和订阅方互相不认识,以后加模块订阅一下就行,零线代码不用动;重算(耗时)和刷新(UI)各跑各的协程,互不卡;零线模块 ...
用并查集识别隧道相邻测线的共有病害
业务场景隧道里有多条平行的测线,相邻两条(编号差 1)上要是各有一个病害、里程范围还重叠,那很可能就是同一个病害在两条测线上各露了一面——这种得标成”共有病害”。
难点在传递性判断这事不能光看两两配对,因为重叠是会传递的:A 和 B 重叠、B 又和 C 重叠,那 A、B、C 其实是同一组,该归到一块儿。要是只两两标记,A-B 一组、B-C 一组,C 跟 A 之间的关系就丢了,分出来的组是碎的。
并查集(Union-Find)这种”沾边就连、要整体分组”的活,正好是并查集擅长的:
class UnionFind(n: Int) { val parent = IntArray(n) { it } val rank = IntArray(n) fun find(x: Int): Int { if (parent[x] != x) parent[x] = find(parent[x]) // 路径压缩 return parent[x] } fun union(a: Int, b: I ...
Excel 导入的两个坑 字段截断与 32767 字符限制
背景做历史层位 / 病害的 Excel 导入导出的时候,连踩了两个”长度上限”的坑,一个在数据库,一个在 Excel 本身,记一下。
坑一:SQLite 字段截断导入历史层位线以后,滚到中间位置,长段的层位线突然变成了一条水平直线。排查发现,存深度数据的 depth_index_array 列定义成了 VARCHAR(5000),超长段的数据被静默截断了——SQLite 写入的时候不报错,读出来就是一截残缺数据,渲染自然就错了。
解决办法:把这列改成 TEXT,不限长度。SQLite 的 TEXT 本身不限长,VARCHAR(N) 里的 N 只是个 hint、并不会真限制(SQLite 弱类型),但有些驱动 / ORM 层会按声明长度截断,所以声明成 TEXT 最保险。
教训就是:别拿 VARCHAR 存可能很长的内容,长文本一律 TEXT,免得被中间某一层按声明长度给截了。
坑二:Excel 单元格字符上限层位导出的时候,原始数据那列把整个深度数组塞进了一个单元格,结果导出直接失败。原因是 Excel 对单个单元格的字符数有硬上限:32767 个字符。深度数组动不 ...
JavaFX 截图卡死 UI 的根治 Java2D 纯数据渲染
痛点病害导出的时候,每条病害要生成一张雷达图截图。原来的做法是直接对着界面上那个 JavaFX 节点调 snapshot() 截图。问题来了:snapshot() 必须在 FX Application Thread 上跑,导出几十上百张图的时候,主线程被这些截图任务占得死死的,整个 UI 完全卡住,进度条都不带动的。
思路卡死的根子在于渲染绑死在 UI 线程上了。要彻底解决,就得让渲染脱离 JavaFX 节点,变成在随便哪个线程上都能跑的纯计算。所以我把截图渲染从”拍 UI”改成了”用 Java2D 拿纯数据画”。
改造按 MVVM 三层切开:
View 层只管从 UI 组件里抠出纯数据(当前视口的里程范围、采样值数组、增益、颜色映射这些),不掺和画图。
ViewModel 层负责渲染的调度和参数拼装,把 View 抠出来的数据组装成绘图参数。
Service 层是纯绘图,用 BufferedImage + Graphics2D 画雷达图、层位线、病害框、信息面板,完全不碰 JavaFX。
// Service 层,任意线程可跑fun render(params: RenderPa ...
