纯 Java 手写 TopoJSON 生成器!零依赖实战教程
目录
3.2 核心转换方法(GeoJSON 转 TopoJSON)
前言
在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 专业知识,只需掌握两点:
- 经纬度坐标格式:`[经度, 纬度]`;
- 多边形/线段由连续坐标点组成,相邻要素共享坐标点即可生成共享弧段。
基于完整工具类实现,支持 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中运行代码可以看到信息有信息输出表示成功,可以看到以下输出:

代码核心说明
- 结构设计:采用工具类设计,继承抽象转换器,支持默认配置和自定义配置,符合 Java 开发规范,可直接集成到 SpringBoot 等项目。
- 核心能力:支持 GeoJSON(Feature/FeatureCollection)转 TopoJSON,自动去重弧段,支持 Polygon、MultiPolygon 两种常用几何类型,贴合实际业务场景。
- 去重逻辑:通过 createArcKey 方法将弧段坐标拼接为唯一字符串,用 Map 记录弧段索引,避免重复存储,实现数据轻量化。
- 便捷性:提供工厂方法快速创建 Feature,丰富的使用示例,降低开发门槛;纯原生实现,无需依赖第三方 GIS 库、JSON 库。
- 可扩展性:可基于此扩展支持 LineString、MultiLineString 等几何类型,优化坐标精度、相对坐标存储等功能。
补充说明:工具类中依赖的 TopologyConverter、AbstractTopologyConverter、TopologyConfig、Feature、Geometry 类,均为自定义简单类,核心是封装配置、要素、几何信息。
四、总结
本文实现了纯 Java、零依赖、可直接运行的 TopoJSON 生成器工具类,相比简易实现,更贴合生产环境需求,核心价值如下:
- 无技术栈限制:纯后端 Java 即可生成,无需 Node.js 等额外环境,无缝集成 Java 后端项目,解决跨语言依赖痛点。
- 轻量化高效:遵循 TopoJSON 拓扑原理,自动去重弧段,大幅减少地理数据体积,提升前端加载速度和数据传输效率。
- 扩展性极强:支持 Polygon、MultiPolygon 类型,可轻松扩展支持更多几何类型、坐标精度配置、相对坐标存储等功能。
- 生产可用:代码结构清晰、注释完善,提供完整使用示例,可直接复制到项目中使用,适配 GIS 开发、数据可视化等多种场景。
基于本工具类,可以将数据库中的经纬度数据、GIS 矢量数据、前端传入的 GeoJSON 数据,直接转换为标准 TopoJSON,用于前端大屏、地图可视化、地理数据分析等场景。后续可进一步优化弧段去重效率、增加坐标压缩、支持更多 GeoJSON 类型,提升工具类的实用性。行文仓促,难免有许多不足之处,如果在实操中遇到问题,欢迎在评论区评论交流~。