Java 新手也能做地图,GeoTools 从零搭建第一个矢量数据处理项目
从零开始:Maven 依赖与环境搭建
对于刚接触 GIS 开发的 Java 程序员来说,最大的拦路虎往往不是算法逻辑,而是繁杂的环境配置。GeoTools 作为一个模块化极强的开源库,其核心功能被拆分成了数十个独立的 Jar 包。如果手动去官网下载所有依赖,不仅容易遗漏,还会面临版本冲突的噩梦。因此,使用 Maven 进行依赖管理是启动项目的唯一正解。
首先,你需要在 pom.xml 中定义 GeoTools 的版本变量。目前社区稳定推荐的版本是 28.x 或更高,为了保持与最新 OGC 标准的兼容,建议跟随官方发布的最新稳定版。在 <properties> 标签中加入:
<properties>
<geotools.version>29.2</geotools.version>
</properties>
接下来是核心的依赖引入。GeoTools 的仓库默认不在 Maven 中央仓库中,因此必须在 <repositories> 节点中添加 OSGeo 的发布源,否则构建时会报错找不到 artifact。
<repositories>
<repository>
<id>osgeo</id>
<name>OSGeo Release Repository</name>
<url>https://repo.osgeo.org/repository/release/</url>
<snapshots>false</snapshots>
<releases>true</releases>
</repository>
</repositories>
依赖包的选择大有讲究。很多新手会直接引入一个巨大的 gt-main,但实际上我们只需要按需加载。对于一个基础的矢量数据处理项目,以下三个核心包是必不可少的:
gt-main:这是 GeoTools 的核心,包含了数据模型、几何对象处理以及基础的数据访问接口。没有它,你甚至无法创建一个简单的 Feature 对象。gt-referencing:GIS 的灵魂在于坐标系。这个包提供了坐标参考系统(CRS)的定义、解析和转换能力。如果你忽略它,后续读取 Shapefile 时很可能会因为无法识别.prj文件而报错,或者导致地图绘制出来是变形的。gt-shapefile:顾名思义,这是专门用于读写 ESRI Shapefile 格式的模块。它是本教程读取本地矢量数据的钥匙。gt-renderer:负责将地理数据渲染为图像。我们要生成缩略图,全靠它。gt-epsg-hsql:这是一个轻量级的 EPSG 代码数据库。GeoTools 需要查询 EPSG 代码(如 4326, 3857)对应的详细参数,这个包提供了基于 HSQL 的本地查找服务,避免每次都要联网查询。
完整的依赖配置如下:
<dependencies>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-main</artifactId>
<version>${geotools.version}</version>
</dependency>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-referencing</artifactId>
<version>${geotools.version}</version>
</dependency>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-shapefile</artifactId>
<version>${geotools.version}</version>
</dependency>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-renderer</artifactId>
<version>${geotools.version}</version>
</dependency>
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-epsg-hsql</artifactId>
<version>${geotools.version}</version>
</dependency>
</dependencies>
配置完成后,执行 mvn clean install。如果一切顺利,你的开发环境就已经就绪了。这一步看似简单,却解决了 90% 初学者“跑不通 Hello World"的问题。
读取 Shapefile 与解析几何属性
环境搭好后,我们立刻进入实战。Shapefile 是 GIS 领域最经典的矢量数据格式,虽然古老,但兼容性极佳。GeoTools 读取 Shapefile 的过程非常直观,主要通过 DataStore 接口来完成。
首先,我们需要指向本地的 .shp 文件路径。注意,Shapefile 实际上是一组文件(.shp, .shx, .dbf, .prj 等),但在代码中只需指定主文件即可,GeoTools 会自动关联同目录下的其他辅助文件。
File shpFile = new File("data/world_countries.shp");
Map<String, Serializable> connect = new HashMap<>();
connect.put("url", DataUtilities.fileToURL(shpFile));
DataStore dataStore = DataStoreFinder.getDataStore(connect);
String typeName = dataStore.getTypeNames()[0];
SimpleFeatureSource featureSource = dataStore.getFeatureSource(typeName);
这里有一个关键细节:DataStoreFinder 会根据传入的参数自动匹配对应的驱动。由于我们引入了 gt-shapefile,它能准确识别 URL 协议并加载 Shapefile 驱动。获取到 SimpleFeatureSource 后,我们就拿到了数据的“只读视图”。
接下来是解析几何对象和属性。GIS 数据与普通数据库表的最大区别在于每一行都包含一个几何字段(Geometry)。我们可以通过 SimpleFeatureCollection 遍历所有要素:
try (SimpleFeatureIterator iterator = featureSource.getFeatures().features()) {
while (iterator.hasNext()) {
SimpleFeature feature = iterator.next();
// 获取属性,例如国家名称
String name = (String) feature.getAttribute("NAME");
// 获取几何对象
Geometry geometry = (Geometry) feature.getDefaultGeometry();
System.out.println("Country: " + name + ", Type: " + geometry.getGeometryType());
// 可以进一步获取边界范围
Envelope envelope = geometry.getEnvelopeInternal();
System.out.println("Bounds: " + envelope);
}
}
在这段代码中,feature.getDefaultGeometry() 返回的是 JTS(Java Topology Suite)的 Geometry 对象。这是 GeoTools 底层依赖的强大几何库,支持点、线、面等各种空间运算。如果你在控制台看到了国家名称和几何类型,说明数据已经成功加载到内存中。此时,数据还只是抽象的对象,我们需要将它们“画”出来才能直观看到效果。
GTRenderer 渲染与 SLD 样式绑定
将内存中的矢量数据转换为图片,是 GeoTools 最迷人的功能之一。这一过程的核心接口是 GTRenderer,而最常用的实现类是 StreamingRenderer。之所以叫“流式”,是因为它在渲染时采用线程池技术,能够高效地处理大规模数据,避免一次性加载所有图形导致内存溢出。
在绘制之前,我们必须解决两个问题:画布大小和样式。
确定绘图范围与画布
地图渲染需要知道“画多大”以及“画哪里”。我们需要从数据中提取最大边界(Bounds),并根据想要的图片宽度计算出高度,以保持纵横比不变形。
SimpleFeatureCollection features = featureSource.getFeatures();
ReferencedEnvelope mapBounds;
try {
mapBounds = features.getBounds();
} catch (Exception e) {
throw new RuntimeException("Failed to calculate bounds", e);
}
// 设定图片宽度为 800 像素,根据经纬度跨度计算高度
int imageWidth = 800;
double heightToWidth = mapBounds.getSpan(1) / mapBounds.getSpan(0);
int imageHeight = (int) Math.round(imageWidth * heightToWidth);
Rectangle imageBounds = new Rectangle(0, 0, imageWidth, imageHeight);
BufferedImage image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_RGB);
SLD 样式绑定
如果没有样式,GeoTools 默认会用随机颜色或单色绘制,效果往往难以辨认。SLD(Styled Layer Descriptor)是 OGC 定义的标准化样式语言,基于 XML 格式。我们可以编写一个简单的 SLD 文件,定义多边形的填充色、边框色和透明度。
假设我们有一个 style.sld 文件,内容定义了淡蓝色的填充和深蓝色的边框。在代码中加载并解析它:
Style style;
try {
File sldFile = new File("style.sld");
style = SLD.parse(sldFile);
} catch (Exception e) {
// 如果解析失败,使用默认样式
style = StyleBuilder.buildDefaultPolygonStyle();
}
解析后的 Style 对象需要绑定到 MapContent 中。MapContent 是 GeoTools 中承载图层、样式和坐标系信息的容器,相当于一个微型的地图工程。
MapContent map = new MapContent();
map.setTitle("World Map Preview");
// 创建图层并添加样式
SimpleFeatureLayer layer = new SimpleFeatureLayer(featureSource, style);
map.addLayer(layer);
执行渲染
万事俱备,最后调用 StreamingRenderer 进行绘制。这里需要注意坐标系的转换,paint 方法需要将地理坐标(世界坐标)映射到屏幕像素坐标。
GTRenderer renderer = new StreamingRenderer();
renderer.setMapContent(map);
// 获取 Graphics2D 对象进行绘制
Graphics2D graphics = image.createGraphics();
// 设置背景色为白色,避免透明背景导致的黑色底图
graphics.setPaint(Color.WHITE);
graphics.fill(imageBounds);
// 执行绘制:将地图范围映射到图片矩形区域
renderer.paint(graphics, imageBounds, mapBounds);
// 保存图片
ImageIO.write(image, "png", new File("output_map.png"));
map.dispose(); // 释放资源
这段代码执行后,你将在项目根目录下看到一张 output_map.png。这就是从数据到图像的完整闭环。
坐标系统与 EPSG 代码处理实战
在实际操作中,你可能会发现生成的图片形状怪异,或者程序抛出 NoSuchAuthorityCodeException 异常。这通常是坐标系(CRS)惹的祸。
Shapefile 通常带有一个 .prj 文件,里面记录了数据的坐标参考系统,例如 GEOGCS["WGS 84"...]。GeoTools 依赖 gt-referencing 和 gt-epsg-hsql 来解析这些字符串。如果缺少 EPSG 数据库支持,GeoTools 就无法理解"WGS 84"对应的是什么数学参数,从而导致渲染失败或静默错误。
确保你的项目中包含了 gt-epsg-hsql 依赖,它会在运行时自动初始化一个内置的 EPSG 查找表。这样,当代码读取到 EPSG:4326 时,就能立即获知这是一个经纬度坐标系,并在渲染时正确处理球面到平面的投影变换。
如果在代码中需要强制指定坐标系(例如数据缺失 .prj 文件),可以使用 CRS.decode 方法手动注入:
CoordinateReferenceSystem sourceCRS = CRS.decode("EPSG:4326");
// 在创建 Layer 时,如果 FeatureSource 的 CRS 为空,可能需要手动处理
// 但通常 SimpleFeatureSource 会自动读取 .prj,无需手动干预
在处理全球范围数据时,直接使用经纬度(EPSG:4326)进行平面渲染会导致高纬度地区严重拉伸。如果需要更美观的预览,可以在渲染前进行动态投影转换,将数据重投影到伪墨卡托(EPSG:3857)或其他适合展示的投影上。不过对于简单的缩略图生成,保持原始坐标系通常也是可以接受的,只要保证长宽比计算正确即可。
n;
}
System.out.println("地图范围: " + mapBounds);
// --- 第三步:创建画布 ---
System.out.println("3. 正在创建画布...");
int imageWidth = 800;
// 根据经纬度范围计算高度,保持地图不变形
double heightToWidth = mapBounds.getSpan(1) / mapBounds.getSpan(0);
int imageHeight = (int) Math.round(imageWidth * heightToWidth);
BufferedImage image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D graphics = image.createGraphics();
// 设置白色背景
graphics.setColor(Color.WHITE);
graphics.fillRect(0, 0, imageWidth, imageHeight);
// 开启抗锯齿,让线条更平滑
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// --- 第四步:创建地图样式(SLD) ---
System.out.println("4. 正在创建地图样式...");
// 使用 GeoTools 的样式工厂创建简单的样式
StyleFactory styleFactory = CommonFactoryFinder.getStyleFactory();
FilterFactory filterFactory = CommonFactoryFinder.getFilterFactory();
// 定义多边形填充色(浅蓝色)和边框(深蓝色)
Fill fill = styleFactory.createFill(filterFactory.literal(Color.decode("#A6CEE3")));
Stroke stroke = styleFactory.createStroke(filterFactory.literal(Color.BLUE), filterFactory.literal(0.5));
PolygonSymbolizer polygonSymbolizer = styleFactory.createPolygonSymbolizer(stroke, fill, null);
Rule rule = styleFactory.createRule();
rule.symbolizers().add(polygonSymbolizer);
FeatureTypeStyle fts = styleFactory.createFeatureTypeStyle(new Rule[]{rule});
Style style = styleFactory.createStyle();
style.featureTypeStyles().add(fts);
// --- 第五步:组装地图内容并渲染 ---
System.out.println("5. 正在渲染地图...");
MapContent mapContent = new MapContent();
mapContent.setTitle("我的第一张地图");
// 将数据源和样式绑定成一个图层
FeatureLayer layer = new FeatureLayer(featureSource, style);
mapContent.addLayer(layer);
// 使用流式渲染器
GTRenderer renderer = new StreamingRenderer();
renderer.setMapContent(mapContent);
// 执行渲染:将地理坐标映射到像素坐标
Rectangle imageBounds = new Rectangle(0, 0, imageWidth, imageHeight);
renderer.paint(graphics, imageBounds, mapBounds);
// --- 第六步:保存图片 ---
System.out.println("6. 正在保存图片...");
File outputFile = new File(outputPath);
ImageIO.write(image, "png", outputFile);
System.out.println("✅ 成功!地图已保存至: " + outputFile.getAbsolutePath());
// 释放资源
mapContent.dispose();
} catch (Exception e) {
System.err.println("生成过程中出现错误:");
e.printStackTrace();
} finally {
// 关闭数据源,释放连接
if (dataStore != null) {
dataStore.dispose();
}
System.out.println("处理完成。");
}
}
}
### 第三步:运行并查看结果
1. 确保你的 `pom.xml` 依赖已正确配置(参考上一节)。
2. 在 IDE 中直接运行 `MapThumbnailGenerator` 类的 `main` 方法。
3. 观察控制台输出,如果看到 “✅ 成功!” 的提示,说明运行成功。
4. 在项目根目录下找到生成的 `world_preview.png` 图片,用图片查看器打开它。
**恭喜!** 你刚刚完成了一个完整的 GIS 数据处理流程:**连接数据 -> 计算范围 -> 设置样式 -> 渲染成图**。这张蓝色的世界地图就是你的第一个作品。
### 常见问题与排查(新手必看)
* **报错:`NoSuchAuthorityCodeException`**
* **原因**:缺少 EPSG 数据库。**解决**:请务必在 `pom.xml` 中添加 `gt-epsg-hsql` 依赖,然后重新运行 `mvn clean install`。
* **报错:`ShapefileDataStore` 相关错误**
* **原因**:`gt-shapefile` 依赖未正确引入或仓库未配置。**解决**:检查 `pom.xml` 中的 `<repositories>` 和 `<dependencies>` 节点,确保 OSGeo 仓库和 `gt-shapefile` 依赖存在。
* **生成的图片是空白或全黑**
* **原因1**:背景色未设置。**解决**:代码中已设置 `graphics.setColor(Color.WHITE)` 和 `fillRect`。
* **原因2**:数据路径错误,没有读取到任何图形。**解决**:检查 `shpPath` 变量指向的文件是否存在,并确认 `.shp`, `.shx`, `.dbf` 文件齐全。
* **地图形状很奇怪,被拉长了**
* **原因**:图片宽高比与地图经纬度范围的比例不一致。**解决**:代码中 `heightToWidth` 的计算就是为了保持比例,请确保使用此动态计算的高度。
### 动手练习:尝试修改
理解代码后,你可以尝试修改以下参数,观察地图的变化:
1. 修改 `imageWidth`(如改为 1200),图片会变宽。
2. 修改 `Fill` 中的颜色值(如 `"#FF9999"` 是浅红色),地图填充色会改变。
3. 修改 `Stroke` 的宽度(如 `filterFactory.literal(1.5)`),国家边界会变粗。
通过这个练习,你不仅跑通了流程,更关键的是**理解了每个步骤的作用**。这比死记硬背 API 要有效得多。
## 生成全球范围缩略图的完整案例
为了让你彻底跑通流程,我们将上述步骤整合成一个完整的工具类方法。这个方法接收一个 Shapefile 路径和一个输出图片路径,自动生成一张带有默认样式的全球范围缩略图。
```java
public class MapThumbnailGenerator {
public static void generateThumbnail(String shpPath, String outputPath) {
File shpFile = new File(shpPath);
if (!shpFile.exists()) {
System.err.println("Shapefile not found: " + shpPath);
return;
}
DataStore dataStore = null;
try {
// 1. 连接数据
Map<String, Serializable> connect = new HashMap<>();
connect.put("url", DataUtilities.fileToURL(shpFile));
dataStore = DataStoreFinder.getDataStore(connect);
String typeName = dataStore.getTypeNames()[0];
SimpleFeatureSource featureSource = dataStore.getFeatureSource(typeName);
// 2. 获取范围
SimpleFeatureCollection features = featureSource.getFeatures();
ReferencedEnvelope mapBounds = features.getBounds();
// 防止范围为空
if (mapBounds.isEmpty()) {
System.err.println("Empty data bounds.");
return;
}
// 3. 准备画布
int width = 1024;
double ratio = mapBounds.getSpan(1) / mapBounds.getSpan(0);
int height = (int) (width * ratio);
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = image.createGraphics();
g2d.setPaint(Color.WHITE);
g2d.fillRect(0, 0, width, height);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// 4. 设置样式 (使用默认多边形样式,实际生产可加载 SLD)
Style style = StyleBuilder.buildDefaultPolygonStyle();
MapContent map = new MapContent();
map.addLayer(new SimpleFeatureLayer(featureSource, style));
map.setMaxBounds(mapBounds);
// 5. 渲染
GTRenderer renderer = new StreamingRenderer();
renderer.setMapContent(map);
renderer.paint(g2d, new Rectangle(0, 0, width, height), mapBounds);
// 6. 输出
ImageIO.write(image, "png", new File(outputPath));
System.out.println("Thumbnail generated successfully: " + outputPath);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (dataStore != null) {
dataStore.dispose();
}
}
}
public static void main(String[] args) {
// 替换为你的实际文件路径
generateThumbnail("data/ne_110m_admin_0_countries.shp", "result/world_preview.png");
}
}
- 点赞
- 收藏
- 关注作者
评论(0)