Android 几何构造器的双栈机:链式 API 底层是怎么装配几何的

points(1,2,3,4,5,6).toLineString() 这样优雅的链式 API,底层其实是一台栈机------压栈、弹栈、装配。

前言

在 GIS 代码里,构造几何对象是件高频又琐碎的事。你要画一条线,得 new LineString(coords);要画一个带洞的多边形,得先造外环、再造内环、最后塞给多边形构造函数。代码又长又容易错。

这个坑很烦:一旦几何出现嵌套(多边形套多边形、集合套集合),你就得在业务代码里手动维护一堆临时变量,把"先造什么、后造什么"的顺序硬编码进去。顺序写错一步,出来的几何就是错的,编译器还拦不住你。

直到看到这样的写法:

kotlin 复制代码
gb.points(0,0, 10,0, 10,10, 0,10, 0,0).ring()
  .points(4,4, 6,4, 6,6, 4,6, 4,4).ring()
  .toPolygon()

一串点进去,一个带洞的多边形就出来了。这套引擎把这种优雅的链式 API 底层实现成了一台双栈装配机。下面拆开看看它是怎么把"散落的坐标"一步步拼成"嵌套几何"的。

一、问题长什么样

假设你正在封装一套几何构造 API,目标是把"原始坐标"变成"各种几何类型"。最朴素的接口是很多个构造函数:

kotlin 复制代码
fun makeLine(coords: List<Coordinate>) = LineString(coords)
fun makeRing(coords: List<Coordinate>) = LinearRing(coords)
fun makePolygon(outer: LinearRing, holes: List<LinearRing>) = Polygon(outer, holes)

用起来还算清楚,但一旦出现嵌套结构(多边形套多边形、集合套集合),调用方就得自己维护一堆临时变量,把"先造什么、后造什么"的顺序硬编码在业务代码里。这种接口不连贯、容易错、也难读

我们想要的是:调用方只按"几何的层次顺序"把事情讲清楚,剩下的装配交给库。怎么做到?

结论先行:嵌套几何的装配顺序,本质上和栈的先进后出天然契合。

二、为什么"栈机"能解决

核心洞察是:几何的嵌套结构和栈的先进后出天然契合。

想象你在拼一个带洞的多边形。自然顺序是:先给外环的坐标,压"外环";再给内环的坐标,压"内环";然后一个"闭合多边形"的动作,把栈顶上同类的环捞出来组装。这不就是栈吗?先压进去的外环留在栈底,后压进去的内环在栈顶,弹出时刚好"先内环后外环",再反转为"先外环后内环"。

于是这个构造器内部维护了两个栈

  • 坐标栈Deque<Coordinate>):存放还没被消费的原始点;
  • 几何栈Deque<Geometry>):存放已经拼好、待进一步组合的中间几何。
kotlin 复制代码
class GeomBuilder {
    private val cstack = ArrayDeque<Coordinate>()   // 坐标栈
    private val gstack = ArrayDeque<Geometry>()     // 几何栈
}

points(1,2,3,4,5,6) 就是把 6 个坐标依次压进坐标栈;ring() 则把所有坐标弹出来、首尾补全闭合、压一个 LinearRing 进几何栈;polygon() 从几何栈顶部连续弹出所有 LinearRing,第一个当外环、其余当内环。

用两个栈记录层次,调用顺序天然对应几何的嵌套关系。

2.1 坐标栈与几何栈的分工

坐标栈只管"还没拼的原始点",几何栈只管"拼好待组装的中间几何"。两个栈各司其职,压栈弹栈的节奏正好对应"先声明材料、再按层次组装"。

2.2 先进后出如何匹配嵌套

先压的外环在栈底、后压的内环在栈顶,弹栈时先拿到内环、反转后外环在前,刚好符合 Polygon(outer, innerRings) 的构造顺序,不用调用方手动排列。

三、解法:压栈、弹栈、按类型装配

核心动作有两个,一个是按类型弹栈 ,一个是闭合环

按类型弹栈polygon() 需要把所有相邻的 LinearRing 一起弹出来。它不能只弹一个,要"从栈顶开始连续弹出所有同类型元素",遇到不同类型才停:

kotlin 复制代码
private fun popAll(clazz: Class<out Geometry>): List<Geometry> {
    val list = ArrayList<Geometry>()
    for (g in gstack) {
        if (clazz.isInstance(g)) list.add(g) else break
    }
    return list.reversed().map { gstack.pop() }
}

依赖栈的"最近先处理"顺序,先压的外环在栈底、后压的内环在栈顶,弹出后刚好外环在前、内环在后,天然符合 Polygon(outer, innerRings) 的构造顺序。

闭合环ring() 弹出所有坐标后,如果首尾点不重合,还要手动补一个首点作为闭合点:

kotlin 复制代码
fun ring(): GeomBuilder {
    var coords = popAll(cstack)
    if (coords.size > 1 && coords.first() != coords.last()) {
        coords = coords + coords.first()
    }
    gstack.push(createLinearRing(coords))
    return this
}

points(...)pointz(...) 这些"压坐标"的入口,全部返回 this,于是整个构造过程可以一路链式点下去,最终用一个 toXxx() 把成品从栈顶弹出。

popAll 按类型连续弹栈,是链式 API 把材料装配成嵌套几何的核心。

3.1 按类型弹栈如何保证顺序

popAll 从栈顶连续取同类元素,遇不同类型即停,再 reversed() 让先压的外环排到前面。这样无论几个内环,都能自动归位到 Polygon 正确的内外环位置。

3.2 闭合环的首尾补全

ring() 必须保证首尾重合才是合法的线性环。弹出的坐标若首 != 尾,就补一个首点,这一步在压栈前完成,外部调用方完全无感。

四、升华:栈机的通用价值

"用栈把嵌套结构串起来"这个思想,远不止几何构造:

  • XML/JSON 的解析器:用栈记录元素的嵌套层级,遇到闭合标签就弹栈组装。
  • 表达式求值:中缀转后缀(逆波兰)就是典型的双栈应用。
  • 撤销/重做:把操作压栈,回退时弹栈逆操作。
  • 任意"先声明材料、再按层次组装"的 DSL:只要产物是嵌套的,栈机几乎总是最自然的实现。

当然,栈机也有代价:它把状态放在两个可变的栈里,调用顺序错了就会在弹栈时报错(比如想弹 LinearRing 栈顶却是 Point)。现代替代是不可变构建器 (每次返回新实例)或类型化构建器(编译期就约束步骤顺序,杜绝错误调用)。但栈机胜在实现直白、性能好,移动端这种轻量场景依然很划算。

双栈机思想可迁移到解析器、表达式求值、撤销重做等一切嵌套结构。

五、结论

  • 几何构造用坐标栈 + 几何栈两台栈机,天然匹配几何的嵌套结构。
  • points 压坐标、ring 闭合并压环、polygon 按类型连续弹栈装配,链式 API 背后是清晰的栈操作。
  • 弹栈时"从栈顶连续弹出同类元素",利用"最近先处理"顺序自动得到外环在前、内环在后。
  • 闭合环时若首尾不重合,需手动补一个首点。
  • "双栈机"思想可迁移到解析器、表达式求值、撤销重做等一切嵌套结构场景。

你写过链式构造几何或类似 DSL 吗?欢迎在评论区聊聊你用的是栈机还是类型化构建器。

相关推荐
mmsx1 小时前
Android 地图卡成 PPT 之后:双渲染管线与空间网格渐进加载怎么救
android·app
光影少年1 小时前
setImmediate 和 setTimeout(0) 的区别
android·前端·react.js·ios·前端框架
Android-Flutter16 小时前
android compose 知识点
android·compose
其实防守也摸鱼17 小时前
Codex 下载与本地部署实战:从安装到运行全指南
android·大数据·运维·安全·自动化
ttyyttemo17 小时前
协程并发执行与共享状态
android
miaowmiaow18 小时前
我让 AI 给老项目做了一次分层架构升级:对标 Now in Android,从「穿层」到「端口与适配器」
android·ai编程·deepseek
mmsx18 小时前
Android 栅格数据缓冲区的类型包装:用匿名子类破解泛型擦除
android·地图·栅格
叶羽西18 小时前
Android Camera HAL调整图像处理线程优先级
android
HouWan19 小时前
Flutter: MediaQuery.of(context) 为什么可能拖慢页面?
android·flutter·ios