Java 新手也能做地图,GeoTools 从零搭建第一个矢量数据处理项目

举报
夜郎king 发表于 2026/10/05 09:01:50 2026/10/05
【摘要】 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,但实际上我们只需要按需加载。对于一个基础的矢量数据处理项目,以下三个核心包是必不可少的:

  1. gt-main:这是 GeoTools 的核心,包含了数据模型、几何对象处理以及基础的数据访问接口。没有它,你甚至无法创建一个简单的 Feature 对象。
  2. gt-referencing:GIS 的灵魂在于坐标系。这个包提供了坐标参考系统(CRS)的定义、解析和转换能力。如果你忽略它,后续读取 Shapefile 时很可能会因为无法识别 .prj 文件而报错,或者导致地图绘制出来是变形的。
  3. gt-shapefile:顾名思义,这是专门用于读写 ESRI Shapefile 格式的模块。它是本教程读取本地矢量数据的钥匙。
  4. gt-renderer:负责将地理数据渲染为图像。我们要生成缩略图,全靠它。
  5. 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");
    }
}
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。