纯 Java 手写 TopoJSON 生成器!零依赖实战教程

纯 Java 手写 TopoJSON 生成器!零依赖实战教程

目录

前言

一、TopoJSON 核心原理极简科普

1.1 TopoJSON 与 GeoJSON 的核心区别

1.2 TopoJSON 核心结构

二、开发环境与前置准备

2.1 开发环境要求

2.2 前置知识点

三、纯 Java 代码实现 TopoJSON 生成

3.1 基础结构与构造函数

3.2 核心转换方法(GeoJSON 转 TopoJSON)

3.3 拓扑构建核心方法

3.4 辅助方法与使用示例

代码核心说明

四、总结


前言

        在GIS(地理信息系统)开发、数据可视化场景中,GeoJSON 是我们最常用的地理数据格式,但它存在一个明显痛点:冗余度极高。比如相邻行政区的公共边界、共享道路,在 GeoJSON 中会被重复存储,导致文件体积大、加载慢。尤其在大数据量场景下,如全国行政区地图、复杂路网可视化,GeoJSON 文件动辄几兆甚至几十兆,极大影响前端加载速度和用户体验,也会增加后端接口的数据传输压力。而 TopoJSON 作为 GeoJSON 的拓扑扩展,通过拓扑结构重构地理数据,将重复的地理线段提取为共享弧段,能让文件体积缩小 80% 以上,是大屏可视化、前端地图渲染的最优解。目前主流的 TopoJSON 生成方案多依赖 Node.js 工具库(如 topojson-server),对于纯 Java 后端技术栈的开发者极不友好:项目无法无缝集成、需要额外部署 Node 环境、跨语言调用增加复杂度,还可能出现数据格式兼容问题。

        因此,本文带你纯 Java 手写 TopoJSON 生成器,全程不依赖任何 GIS 库、第三方 JSON 库,原生 JDK 即可运行,真正实现后端零成本生成标准 TopoJSON 数据,适配 Java 后端项目的无缝集成需求,解决跨语言依赖的痛点,同时帮助开发者深入理解 TopoJSON 的底层实现逻辑。

一、TopoJSON 核心原理极简科普

1.1 TopoJSON 与 GeoJSON 的核心区别

        要理解 TopoJSON 的优势,首先要明确它与 GeoJSON 的核心差异——本质是「数据存储方式的不同」。GeoJSON 采用「独立存储」模式,每个地理要素(如多边形、线段)都完整存储自身的所有坐标点,即便两个要素共享一段边界(比如两个相邻的省份),这段边界的坐标也会在两个要素中分别存储一次。这种方式的优点是结构简单、易于理解,但缺点也极其明显:数据冗余严重,要素越多、共享边界越多,冗余度越高,文件体积就越大。

        而 TopoJSON 采用「共享复用」模式,核心思路是「提取共性、复用弧段」。它会先遍历所有地理要素,将其中重复出现的线段(即共享边界、共享道路等)提取出来,封装成「弧段(Arc)」,再通过索引引用的方式,将这些弧段组合成各个地理要素。简单理解:GeoJSON 是「每个图形单独画,重复线条重复画」,TopoJSON 是「先画所有公共线条,再用这些线条拼接成每个图形」,从根源上消除了数据冗余。举个通俗例子:两个相邻的正方形,GeoJSON 会存储两个正方形的8个顶点(每个正方形4个,共享边的2个顶点重复存储);而 TopoJSON 会提取出共享的那条边作为一个弧段,再用这个弧段+各自的非共享边,组合成两个正方形,仅存储6个顶点,冗余度直接降低30%以上,数据量越大,这个优势越明显。

1.2 TopoJSON 核心结构

        标准 TopoJSON 是一个 JSON 对象,包含三个核心部分(其中 arcs 和 objects 是必选,其他为可选),理解这三个部分,就能轻松掌握 TopoJSON 的生成逻辑,也是我们手写生成器的核心依据。第一部分是「type」,固定值为「Topology」,用于标识当前 JSON 是一个 TopoJSON 拓扑对象,区别于 GeoJSON 的「Feature」「FeatureCollection」等类型,这是 TopoJSON 的标志性标识,不可或缺。第二部分是「arcs(弧段集)」,这是 TopoJSON 轻量化的核心,本质是一个二维数组:外层数组存储所有共享弧段,每个弧段是一个包含多个坐标点的数组,坐标点格式与 GeoJSON 一致,均为「[经度, 纬度]」。需要注意的是,弧段的坐标采用「相对坐标」存储(部分实现),即每个坐标点相对于前一个坐标点的偏移量,进一步压缩文件体积;但我们手写实现时,可先采用绝对坐标,降低难度,后续可按需优化为相对坐标。第三部分是「objects(要素集)」,用于存储具体的地理要素,每个要素通过「索引引用」的方式关联 arcs 中的弧段,组合成完整的地理图形(点、线、面)。例如,一个多边形要素,其 geometry 中的 arcs 属性,存储的不是具体坐标,而是 arcs 数组的索引(如 [0] 表示引用第一个弧段),如果是复杂多边形,可能需要引用多个弧段,甚至通过正负索引表示弧段的方向(正索引表示顺时针,负索引表示逆时针),用于区分多边形的内外环。此外,TopoJSON 还可包含「bbox(边界范围)」「transform(坐标变换参数)」等可选属性,用于优化数据存储和解析,但核心还是 arcs 和 objects 的组合,这也是我们手写生成器需要重点实现的部分。

二、开发环境与前置准备

2.1 开发环境要求

        本项目零依赖、纯原生,仅需基础 JDK 环境即可运行:

  • JDK 1.8 及以上(推荐 1.8+,所有版本通用)
  • 任意 Java 开发工具(IDEA/Eclipse/记事本均可)

2.2 前置知识点

        无需 GIS 专业知识,只需掌握两点:

  1. 经纬度坐标格式:`[经度, 纬度]`;
  2. 多边形/线段由连续坐标点组成,相邻要素共享坐标点即可生成共享弧段。

        基于完整工具类实现,支持 Polygon、MultiPolygon 类型,自动去重弧段,贴合实际开发场景,生成标准可验证的 TopoJSON。

三、纯 Java 代码实现 TopoJSON 生成

        前面了解了 TopoJSON 的核心原理,接下来直接上「生产级简易工具类」,拆分核心模块实现,代码可直接复制到项目中使用,支持 GeoJSON 转 TopoJSON、多要素处理、弧段自动去重,全程零依赖第三方库。

3.1 基础结构与构造函数

        先搭建工具类的基础结构,继承抽象转换器(可自行实现简单抽象类),定义核心常量和构造函数,支持默认配置和自定义配置,适配不同场景需求。

package com.example; import com.example.model.Feature; import com.example.model.Geometry; import java.io.IOException; import java.util.*; /** * TopoJSON 生成器工具类(基础版本) * * * 实现 {@link TopologyConverter} 接口,使用纯 Java 实现将 GeoJSON 格式转换为 TopoJSON 格式。 * 不依赖任何第三方地理空间库 * * * 主要功能: * * 将 GeoJSON FeatureCollection 转换为 TopoJSON * 将 GeoJSON Feature 转换为 TopoJSON * 支持 Polygon 和 MultiPolygon 几何类型 * 自动去重相同的弧段以减小数据体积 * * * @author TopoJSON Generator * @version 2.0.0 * @see TopologyConverter * @see AbstractTopologyConverter * @see TopologyConfig */ public class TopoJsonGenerator extends AbstractTopologyConverter { /** 转换器名称 */ public static final String NAME = "TopoJsonGenerator"; /** 转换器版本 */ public static final String VERSION = "2.0.0"; /** * 默认构造函数 * * 使用默认配置创建转换器 */ public TopoJsonGenerator() { super(); } /** * 带配置的构造函数 * * @param config 转换配置(如精度、是否复制属性等) */ public TopoJsonGenerator(TopologyConfig config) { super(config); } /** * 获取转换器名称(实现接口方法) * * @return 转换器名称 "TopoJsonGenerator" */ @Override public String getName() { return NAME; } }

3.2 核心转换方法(GeoJSON 转 TopoJSON)

        这部分是工具类的核心,实现 GeoJSON 字符串到 TopoJSON 字符串、TopoJSON Map 对象的转换,支持 Feature 和 FeatureCollection 两种 GeoJSON 类型,适配主流使用场景。

 /** * 将 GeoJSON 字符串转换为 TopoJSON 字符串 * * @param geoJson GeoJSON 格式的字符串 * @return TopoJSON 格式的字符串 * @throws RuntimeException 如果转换失败(如解析异常) */ @Override public String convertGeoJsonToTopoJson(String geoJson) { try { // 先将 GeoJSON 转换为 TopoJSON Map 结构 Map<String, Object> topology = convertGeoJsonToTopology(geoJson); // 将 Map 结构转为 JSON 字符串(toJson 方法需自行实现,纯原生无依赖) return toJson(topology); } catch (IOException e) { throw new RuntimeException("Failed to convert GeoJSON to TopoJSON", e); } } /** * 将 GeoJSON 字符串转换为 TopoJSON Map 对象(便于后续灵活处理) * * @param geoJson GeoJSON 格式的字符串 * @return TopoJSON 格式的 Map 对象 * @throws IllegalArgumentException 如果 GeoJSON 类型不支持(仅支持 Feature/FeatureCollection) */ @Override public Map<String, Object> convertGeoJsonToTopology(String geoJson) { try { // 解析 GeoJSON 字符串为 Map(parseJson 方法需自行实现,纯原生无依赖) Map<String, Object> geoJsonMap = parseJson(geoJson); String type = (String) geoJsonMap.get("type"); // 处理 FeatureCollection 类型(多个要素) if ("FeatureCollection".equals(type)) { @SuppressWarnings("unchecked") List<Map<String, Object>> featuresList = (List<Map<String, Object>>) geoJsonMap.get("features"); return createTopologyFromFeatures(featuresList); } // 处理单个 Feature 类型 else if ("Feature".equals(type)) { List<Map<String, Object>> singleFeature = new ArrayList<Map<String, Object>>(); singleFeature.add(geoJsonMap); return createTopologyFromFeatures(singleFeature); } // 不支持的 GeoJSON 类型 else { throw new IllegalArgumentException("Unsupported GeoJSON type: " + type); } } catch (IOException e) { throw new RuntimeException("Failed to parse GeoJSON", e); } }

3.3 拓扑构建核心方法(弧段去重+要素组装)

        这部分实现 TopoJSON 最核心的逻辑:将 GeoJSON 的 Feature 列表转换为 TopoJSON 结构,自动提取弧段、去重弧段,通过索引引用弧段,从根源实现数据轻量化。

 /** * 将 GeoJSON Feature 列表(Map 形式)转换为 TopoJSON Map 结构 * 内部将 Map 形式的 Feature 转为自定义 Feature 对象,便于后续处理 * * @param featuresList Feature 列表的 Map 表示 * @return TopoJSON 格式的 Map 结构 */ private Map<String, Object> createTopologyFromFeatures(List<Map<String, Object>> featuresList) { List<Feature> features = new ArrayList<Feature>(); // 遍历 Map 形式的 Feature,转为自定义 Feature 对象(封装 ID、几何信息、属性) for (Map<String, Object> featureMap : featuresList) { // 获取要素 ID(优先从配置的 ID 字段获取,无则用默认 ID) String id = (String) featureMap.get(config.getIdProperty()); if (id == null) { id = (String) featureMap.get("id"); } // 获取要素属性(支持配置是否复制属性) @SuppressWarnings("unchecked") Map<String, Object> properties = (Map<String, Object>) featureMap.get("properties"); if (config.isCopyProperties() && properties == null) { properties = new HashMap<String, Object>(); } // 解析几何信息(类型+坐标),封装为 Geometry 对象 @SuppressWarnings("unchecked") Map<String, Object> geometryMap = (Map<String, Object>) featureMap.get("geometry"); String geometryType = (String) geometryMap.get("type"); Object coordinates = geometryMap.get("coordinates"); Geometry geometry = new Geometry(geometryType, coordinates); features.add(new Feature(id, geometry, properties)); } // 调用核心方法,从 Feature 列表创建 TopoJSON return createTopology(features); } /** * 从 Feature 列表创建 TopoJSON 结构(核心实现) * * * 核心逻辑: * * 1. 提取所有弧段,用 Map 记录弧段唯一标识,实现自动去重 * 2. 用 arcs 数组存储所有唯一弧段,用索引关联 * 3. 用 objects 对象存储要素,通过弧段索引引用 arcs 中的弧段 * * * * @param features Feature 对象列表 * @return TopoJSON 格式的 Map 结构 */ public Map<String, Object> createTopology(List<Feature> features) { // 存储完整 TopoJSON 结构,用 LinkedHashMap 保证顺序 Map<String, Object> topology = new LinkedHashMap<String, Object>(); topology.put("type", "Topology"); // TopoJSON 标志性类型 // 存储所有唯一弧段(arcs 核心数组) List<List<List<Double>>> arcs = new ArrayList<List<List<Double>>>(); // 弧段去重核心:key=弧段唯一标识,value=弧段在 arcs 中的索引 Map<String, Integer> arcIndexMap = new HashMap<String, Integer>(); int arcIndex = 0; // 弧段索引计数器 // 存储 TopoJSON 的 objects 部分(要素集合) Map<String, Object> objects = new LinkedHashMap<String, Object>(); // 遍历每个 Feature,处理其几何信息,提取弧段 for (Feature feature : features) { // 生成要素 ID(无 ID 则自动生成) String featureId = feature.getId() != null ? feature.getId() : "feature_" + objects.size(); // 单个要素对象,存储该要素的类型、弧段引用、属性 Map<String, Object> object = new LinkedHashMap<String, Object>(); object.put("type", feature.getGeometry().getType()); // 处理 Polygon 类型(多边形) if (feature.getGeometry().getType().equals("Polygon")) { @SuppressWarnings("unchecked") List<List<List<Double>>> rings = (List<List<List<Double>>>) feature.getGeometry().getCoordinates(); List<List<Integer>> arcsList = new ArrayList<List<Integer>>(); // 遍历多边形的每个环(外环+内环),提取弧段 for (List<List<Double>> ring : rings) { // 生成弧段唯一标识,用于去重 String arcKey = createArcKey(ring); Integer existingArcIndex = arcIndexMap.get(arcKey); // 弧段已存在,直接引用其索引 if (existingArcIndex != null) { arcsList.add(Collections.singletonList(existingArcIndex)); } // 弧段不存在,添加到 arcs 数组,记录索引 else { arcsList.add(Collections.singletonList(arcIndex)); arcs.add(ring); arcIndexMap.put(arcKey, arcIndex); arcIndex++; } } // 关联该多边形的弧段索引 object.put("arcs", arcsList); } // 处理 MultiPolygon 类型(多多边形) else if (feature.getGeometry().getType().equals("MultiPolygon")) { @SuppressWarnings("unchecked") List<List<List<List<Double>>>> polygons = (List<List<List<List<Double>>>>) feature.getGeometry().getCoordinates(); List<List<List<Integer>>> arcsList = new ArrayList<List<List<Integer>>>(); // 遍历每个多边形 for (List<List<List<Double>>> polygon : polygons) { List<List<Integer>> polygonArcs = new ArrayList<List<Integer>>(); // 遍历每个多边形的环,提取弧段 for (List<List<Double>> ring : polygon) { String arcKey = createArcKey(ring); Integer existingArcIndex = arcIndexMap.get(arcKey); if (existingArcIndex != null) { polygonArcs.add(Collections.singletonList(existingArcIndex)); } else { polygonArcs.add(Collections.singletonList(arcIndex)); arcs.add(ring); arcIndexMap.put(arcKey, arcIndex); arcIndex++; } } arcsList.add(polygonArcs); } // 关联该多多边形的弧段索引 object.put("arcs", arcsList); } // 若要素有属性,添加到要素对象中 if (feature.getProperties() != null && !feature.getProperties().isEmpty()) { object.put("properties", feature.getProperties()); } // 将要素添加到 objects 中 objects.put(featureId, object); } // 组装完整 TopoJSON(arcs + objects) topology.put("arcs", arcs); topology.put("objects", objects); return topology; }

3.4 辅助方法与使用示例

        补充弧段唯一键生成方法(用于去重)、便捷工厂方法(快速创建 Feature),以及完整使用示例,复制即可运行,降低使用门槛。

System.out.println("=== Test 1: Basic Converter ==="); TopologyConverter converter = new TopoJsonGenerator(); System.out.println("Converter: " + converter.getName() + " v" + converter.getVersion()); String geoJson = "{\"type\":\"FeatureCollection\",\"features\":[" + "{\"type\":\"Feature\",\"id\":\"f1\",\"properties\":{\"name\":\"China\",\"population\":1400000000}," + "\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[73,18],[135,18],[135,54],[73,54],[73,18]]]}}," + "{\"type\":\"Feature\",\"id\":\"f2\",\"properties\":{\"name\":\"Japan\",\"population\":126000000}," + "\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[129,30],[146,30],[146,46],[129,46],[129,30]]]}}" + "]}"; String topoJson = converter.convertGeoJsonToTopoJson(geoJson); System.out.println("Result:"); System.out.println(topoJson); System.out.println();

        在IDE中运行代码可以看到信息有信息输出表示成功,可以看到以下输出:

        代码核心说明

  1. 结构设计:采用工具类设计,继承抽象转换器,支持默认配置和自定义配置,符合 Java 开发规范,可直接集成到 SpringBoot 等项目。
  2. 核心能力:支持 GeoJSON(Feature/FeatureCollection)转 TopoJSON,自动去重弧段,支持 Polygon、MultiPolygon 两种常用几何类型,贴合实际业务场景。
  3. 去重逻辑:通过 createArcKey 方法将弧段坐标拼接为唯一字符串,用 Map 记录弧段索引,避免重复存储,实现数据轻量化。
  4. 便捷性:提供工厂方法快速创建 Feature,丰富的使用示例,降低开发门槛;纯原生实现,无需依赖第三方 GIS 库、JSON 库。
  5. 可扩展性:可基于此扩展支持 LineString、MultiLineString 等几何类型,优化坐标精度、相对坐标存储等功能。

        补充说明:工具类中依赖的 TopologyConverter、AbstractTopologyConverter、TopologyConfig、Feature、Geometry 类,均为自定义简单类,核心是封装配置、要素、几何信息。

四、总结

        本文实现了纯 Java、零依赖、可直接运行的 TopoJSON 生成器工具类,相比简易实现,更贴合生产环境需求,核心价值如下:

  1. 无技术栈限制:纯后端 Java 即可生成,无需 Node.js 等额外环境,无缝集成 Java 后端项目,解决跨语言依赖痛点。
  2. 轻量化高效:遵循 TopoJSON 拓扑原理,自动去重弧段,大幅减少地理数据体积,提升前端加载速度和数据传输效率。
  3. 扩展性极强:支持 Polygon、MultiPolygon 类型,可轻松扩展支持更多几何类型、坐标精度配置、相对坐标存储等功能。
  4. 生产可用:代码结构清晰、注释完善,提供完整使用示例,可直接复制到项目中使用,适配 GIS 开发、数据可视化等多种场景。

        基于本工具类,可以将数据库中的经纬度数据、GIS 矢量数据、前端传入的 GeoJSON 数据,直接转换为标准 TopoJSON,用于前端大屏、地图可视化、地理数据分析等场景。后续可进一步优化弧段去重效率、增加坐标压缩、支持更多 GeoJSON 类型,提升工具类的实用性。行文仓促,难免有许多不足之处,如果在实操中遇到问题,欢迎在评论区评论交流~。

Read more

AirSim无人机仿真入门(一):实现无人机的起飞与降落

AirSim无人机仿真入门(一):实现无人机的起飞与降落

概述: 安装好所需要的软件和环境,通过python代码控制无人机进行起飞和降落。 参考资料: 1、知乎宁子安大佬的AirSim教程(文字教程,方便复制) 2、B站瑜瑾玉大佬的30天RL无人机仿真教程(视频教程,方便理解) 3、AirSim官方手册(资料很全,不过是纯英文的) AirSim无人机仿真入门(一):实现无人机的起飞与降落 * 1 安装AirSim * 1.1 参考教程 * 1.2 内容梳理 * 1.3 步骤总结 * 2 开始使用 AirSim * 2.1 参考教程 * 2.2 内容梳理 * 2.3 步骤总结 * 3 撰写python控制程序 * 3.1 参考教程 * 3.2 内容梳理

By Ne0inhk

OpenClaw 安装 + 接入飞书机器人完整教程

OpenClaw 安装 + 接入飞书机器人完整教程 OpenClaw 曾用名:ClawdBot → MoltBot → OpenClaw(同一软件,勿混淆) 适用系统:Windows 10/11 最后更新:2026年3月 一、什么是 OpenClaw? OpenClaw 是一款 2026 年爆火的开源个人 AI 助手,GitHub 星标已超过 10 万颗。 与普通 AI 聊天机器人的核心区别: * 真正的执行能力:不只回答问题,能实际操作你的电脑 * 24/7 全天候待命:睡觉时也能主动完成任务 * 完全开源免费:数据完全掌控在自己手中 * 支持国内平台:飞书、钉钉等均已支持接入 二、安装前准备:安装 Node.js 建议提前手动安装

By Ne0inhk
【机器人】复现 StreamVLN 具身导航 | 流式VLN | 连续导航

【机器人】复现 StreamVLN 具身导航 | 流式VLN | 连续导航

StreamVLN 通过在线、多轮对话的方式,输入连续视频,输出动作序列。 通过结合语言指令、视觉观测和空间位姿信息,驱动模型生成导航动作(前进、左转、右转、停止)。 论文地址:StreamVLN: Streaming Vision-and-Language Navigation via SlowFast Context Modeling 代码地址:https://github.com/OpenRobotLab/StreamVLN 本文分享StreamVLN 复现和模型推理的过程~ 下面是示例效果: 1、创建Conda环境 首先创建一个Conda环境,名字为streamvln,python版本为3.9; 然后进入streamvln环境,执行下面命令: conda create -n streamvln python=3.9 conda activate streamvln 2、 安装habitat仿真环境

By Ne0inhk
OpenClaw配置Bot接入飞书机器人+Kimi2.5

OpenClaw配置Bot接入飞书机器人+Kimi2.5

上一篇文章写了Ubuntu_24.04下安装OpenClaw的过程,这篇文档记录一下接入飞书机器+Kimi2.5。 准备工作 飞书 创建飞书机器人 访问飞书开放平台:https://open.feishu.cn/app,点击创建应用: 填写应用名称和描述后就直接创建: 复制App ID 和 App Secret 创建成功后,在“凭证与基础信息”中找到 App ID 和 App Secret,把这2个信息复制记录下来,后面需要配置到openclaw中 配置权限 点击【权限管理】→【开通权限】 或使用【批量导入/导出权限】,选择导入,输入以下内容,如下图 点击【下一步,确认新增权限】即可开通所需要的权限。 配置事件与回调 说明:这一步的配置需要先讲AppId和AppSecret配置到openclaw成功之后再设置订阅方式,

By Ne0inhk