用 LambdaiCJ 迁移 gcoord
最近试着用 LambdaiCJ 做了一个 gcoord 的仓颉版。
gcoord 是一个地理坐标系转换库,原版由 TypeScript 实现,支持 WGS84、GCJ02、BD09、BD09MC、EPSG3857 等常见坐标系之间的转换。我的目标不是重新设计它,而是尽量按照原项目的结构和测试思路,把核心能力迁移到仓颉里。
参考的 TypeScript 项目结构
原仓库真正需要关注的是 src/ 和 test/。
gcoord/
├─ src/
│ ├─ index.ts # JS/TS 对外入口
│ ├─ transform.ts # 统一转换 API
│ ├─ helper.ts # 工具函数 + GeoJSON 坐标遍历
│ ├─ geojson.ts # GeoJSON 类型定义
│ └─ crs/ # 各坐标系转换公式
│ ├─ index.ts # CRS 枚举、别名、转换路由表
│ ├─ GCJ02.ts # WGS84 <-> GCJ02
│ ├─ BD09.ts # GCJ02 <-> BD09
│ ├─ BD09MC.ts # BD09 <-> BD09MC
│ ├─ EPSG3857.ts # WGS84 <-> EPSG3857/WebMercator
│ └─ WGS84.ts # 空壳注释文件
│
├─ test/
│ ├─ fixtures/
│ │ ├─ china-cities.json # 坐标样本数据
│ │ └─ each.ts # 测试读取 fixture 的辅助函数
│ ├─ helpers/
│ │ └─ geojson.ts # 构造 GeoJSON 的辅助函数
│ └─ unit/
│ ├─ transform.spec.ts # transform 总入口测试
│ ├─ helper.spec.ts # helper / coordEach 测试
│ └─ crs/ # 各 CRS 转换测试
其中 transform.ts 是核心入口。
export default function transform<T extends GeoJSON | Position>(
input: T | string,
crsFrom: CRSTypes,
crsTo: CRSTypes
): T
它大致支持三类输入:
- 普通坐标数组:
[lng, lat] - GeoJSON 对象
- GeoJSON 字符串
处理流程也很直接:先检查输入和坐标系是否合法,如果 from == to 就原样返回;否则从 crsMap 里找到对应的转换函数。输入是字符串时先 JSON.parse,输入是坐标数组时直接转换,输入是 GeoJSON 时则遍历所有坐标并原地修改。
第一阶段:先迁移坐标数组
一开始我没有直接做完整 GeoJSON 支持,而是先把坐标数组的转换跑通。第一版目录大概是这样:
gcoord_cj/
├─ cjpm.toml # 仓颉项目配置
├─ README.md # 使用说明
├─ CHANGELOG.md # 版本变更记录
├─ LICENSE # 开源许可证
├─ src/
│ ├─ transform.cj # 对外 transform API
│ ├─ helper/
│ │ └─ helper.cj # 断言、函数组合、数值辅助等
│ ├─ geojson/
│ │ └─ geojson.cj # 第一阶段只定义 Position
│ └─ crs/
│ ├─ index.cj # CRSType 与转换路由
│ ├─ gcj02.cj
│ ├─ bd09.cj
│ ├─ bd09mc.cj
│ ├─ epsg3857.cj
│ └─ wgs84.cj
├─ test/
│ └─ china-cities.json # 从原项目保留的坐标样本数据
└─ scripts/
└─ smoke_test.sh # 快速构建或手动冒烟验证脚本
这里踩到的第一个点是:仓颉项目并不一定需要 index.cj 作为唯一入口文件,而是可以直接根据包路径暴露能力。所以我最后把 transform.cj 作为主要入口。
当时设计的暴露边界是:
public:transform()、Position、CRSTypeprotected:getConverter()、各 CRS 公式函数、helper里的TransformFn/assert/compose
根包里的 transform.cj 负责转发 gcoord.geojson.* 和 gcoord.crs.*,同时内部调用 gcoord.crs.getConverter 做路由。这样外部用户仍然只需要:
import gcoord.*
另外,仓颉库项目需要把 cjpm.toml 里的输出类型从可执行程序改成静态库:
[package]
output-type = "static"
其他项目使用时再通过本地路径依赖引入:
[dependencies]
gcoord = { path = "../gcoord_cj" }
第一轮迁移后,几个核心模块基本成型:
transform.cj:完成transform()的迁移,通过重载支持GeoPosition和字符串输入,暂时不处理完整 GeoJSON 对象helper/helper.cj:按 TypeScript 原实现迁移assert()、compose()等工具函数geojson/geojson.cj:先定义GeoPosition类型crs/index.cj:把枚举换成String类型的CRSType,定义坐标系常量、别名和双层 HashMap 转换路由表
关于 Position 重名
迁移过程中还遇到过一次看起来像导入机制的问题。
我一开始以为,同 package 下在多个文件里 import 同一个 Position 类型,会在函数顶级签名里造成重复导入。后来发现真实原因不是这个,而是 import lambdai4cj.prelude.* 里也有一个叫 Position 的类型,导致了重名。
最后的解决方式很简单:把自己的坐标类型改名为 GeoPosition。
用 LambdaiCJ 迁移计算函数
坐标转换里最适合交给 AI 的部分,其实是纯运算函数。
我的做法是先把常量和函数依赖关系整理好,再让 @ai 模块参照原 TypeScript 文件生成仓颉代码:@files 里放源码文件,@AIDeps 描述原函数调用关系,再从 china-cities.json 中挑几个样本作为 @test。
这类任务涉及的仓颉语言特性不算多,更多是公式和数值误差控制,所以成功率比预期高。真正需要人工盯住的地方,反而是项目边界、包结构、公开入口和测试组织。
第二阶段:补上 GeoJSON 支持
后来我开始继续加 GeoJSON 支持。
最初的想法是照搬 TypeScript 的类型检查,但很快发现这条路不太顺。仓颉没有 TypeScript 那种根据返回值类型参与推断函数重载的写法,强行模仿原版的类型结构只会把代码变复杂。
找到仓颉 stdx 里的 JSON 支持后,我换了一个方案:不再强行还原 TS 类型系统,而是直接对 JsonValue 递归遍历。遇到坐标数组就转换,其他结构原样保留。
这样会少一些静态类型约束,但代码干净很多,也更符合这个阶段的目标:先让任意 GeoJSON 对象能完成坐标转换。
整理后的 transform.cj 入口变成了这样:
public func transform<T>(input: Array<T>, crsFrom: CRSType, crsTo: CRSType): Array<Float64> where T <: ToString {
return transformArrayPosition(input, crsFrom, crsTo)
}
public func transform(input: String, crsFrom: CRSType, crsTo: CRSType): Any {
let value = JsonValue.fromStr(input)
match (value.kind()) {
case JsArray =>
return transformJsonPosition(value.asArray(), crsFrom, crsTo)
case JsObject =>
transformJsonGeoJSONInPlace(value.asObject(), crsFrom, crsTo)
return value
case _ =>
throw IllegalArgumentException("Invalid input coordinate: ${input}")
}
}
public func transform(input: JsonValue, crsFrom: CRSType, crsTo: CRSType): JsonValue {
match (input.kind()) {
case JsArray =>
let output = transformJsonPosition(input.asArray(), crsFrom, crsTo)
return JsonArray(output.map<JsonValue>({ value => JsonFloat(value) }))
case JsObject =>
transformJsonGeoJSONInPlace(input.asObject(), crsFrom, crsTo)
return input
case _ =>
throw IllegalArgumentException("Invalid input coordinate: ${input}")
}
}
这时目录结构也随之调整:
gcoord_cj/src/
├─ transform.cj
│ └─ 只放公开 transform 重载,负责分派 Array / String / JsonValue 输入
│
├─ coord/
│ └─ position.cj
│ ├─ GeoPosition
│ ├─ Array<T> -> GeoPosition
│ ├─ JsonArray -> Array<Float64>
│ └─ JsonArray 坐标写回工具
│
├─ geojson/
│ └─ json_transform.cj
│ ├─ JsonValue / JsonObject GeoJSON 原地转换逻辑
│ ├─ Feature / FeatureCollection
│ ├─ Point / LineString / Polygon
│ ├─ MultiPoint / MultiLineString / MultiPolygon
│ └─ GeometryCollection
│
├─ crs/
│ ├─ index.cj
│ │ ├─ CRSType 常量和别名
│ │ ├─ CRS 转换矩阵
│ │ ├─ assertCRS()
│ │ └─ getTransformFn()
│ ├─ wgs84.cj
│ ├─ gcj02.cj
│ ├─ bd09.cj
│ ├─ bd09mc.cj
│ └─ epsg3857.cj
│
└─ helper/
└─ helper.cj
└─ assert / compose 等通用工具
公开给用户使用的入口仍然只保留 transform。
测试与收尾
最后让 GPT 根据 gcoord/test/ 里的测试思路补了一个 shell 测试脚本。这个脚本还真测出了一个问题:仓颉的 toString() 在某些数值转换场景下会带来精度丢失。
后来加了一个兼容函数 toFloat64(),问题就解决了。
这次迁移下来,我对 LambdaiCJ 的感觉是:它很适合帮忙迁移纯运算函数,尤其是已有源码、调用关系和测试样本都比较明确的时候。但包结构、公开 API、依赖边界、测试策略这些东西,还是需要人站在项目层面盯着。
换句话说,AI 可以很快把“代码块”写出来,但一个库能不能真的像库一样被别人使用,仍然取决于你有没有把入口、边界和验证方式想清楚。