目录
[一、TopoJSON 核心原理极简科普](#一、TopoJSON 核心原理极简科普)
[1.1 TopoJSON 与 GeoJSON 的核心区别](#1.1 TopoJSON 与 GeoJSON 的核心区别)
[1.2 TopoJSON 核心结构](#1.2 TopoJSON 核心结构)
[2.1 开发环境要求](#2.1 开发环境要求)
[2.2 前置知识点](#2.2 前置知识点)
[三、纯 Java 代码实现 TopoJSON 生成](#三、纯 Java 代码实现 TopoJSON 生成)
[3.1 基础结构与构造函数](#3.1 基础结构与构造函数)
[3.2 核心转换方法(GeoJSON 转 TopoJSON)](#3.2 核心转换方法(GeoJSON 转 TopoJSON))
[3.3 拓扑构建核心方法](#3.3 拓扑构建核心方法)
[3.4 辅助方法与使用示例](#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 专业知识,只需掌握两点:
-
经纬度坐标格式:`[经度, 纬度]`;
-
多边形/线段由连续坐标点组成,相邻要素共享坐标点即可生成共享弧段。
基于完整工具类实现,支持 Polygon、MultiPolygon 类型,自动去重弧段,贴合实际开发场景,生成标准可验证的 TopoJSON。
三、纯 Java 代码实现 TopoJSON 生成
前面了解了 TopoJSON 的核心原理,接下来直接上「生产级简易工具类」,拆分核心模块实现,代码可直接复制到项目中使用,支持 GeoJSON 转 TopoJSON、多要素处理、弧段自动去重,全程零依赖第三方库。
3.1 基础结构与构造函数
先搭建工具类的基础结构,继承抽象转换器(可自行实现简单抽象类),定义核心常量和构造函数,支持默认配置和自定义配置,适配不同场景需求。
java
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 类型,适配主流使用场景。
java
/**
* 将 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 结构,自动提取弧段、去重弧段,通过索引引用弧段,从根源实现数据轻量化。
java
/**
* 将 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),以及完整使用示例,复制即可运行,降低使用门槛。
java
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 类型,提升工具类的实用性。行文仓促,难免有许多不足之处,如果在实操中遇到问题,欢迎在评论区评论交流~。