Apache Commons Geometry 快速入门与集成示例

Apache Commons Geometry 1.0 快速入门与集成示例,覆盖 Maven 依赖引入、2D 向量/直线/线段/区域运算、3D 平面/直线相交、点定位与精度容差控制,全部示例代码真实编译运行并给出完整回显。

Apache Commons GeometryApache Commons Geometry2026/10/14

Apache Commons Geometry 快速入门与集成示例

目标:用最短路径跑通 Apache Commons Geometry 1.0——从 Maven 引入依赖到 2D/3D 几何运算与点定位的真实运行,提供可直接复制的完整示例代码与实测回显。以下全部代码与输出均在 Ubuntu 22.04(JDK 17.0.20.1 + Maven 3.6.3)实测通过。

一、环境信息

本节先固定本次实测所用软硬件基线,便于读者逐项对照复现。

  • 操作系统:Ubuntu 22.04.5 LTS(x86_64)
  • JDK:OpenJDK 17.0.20.1
  • 构建工具:Apache Maven 3.6.3
  • 目标库:Apache Commons Geometry 1.0(org.apache.commons:commons-geometry-euclidean:1.0)
  • 示例工程目录:/opt/geo-demo
  • 运行期堆内存:示例统一 -Xmx256m
  • 源码编码:UTF-8(project.build.sourceEncoding)

二、前置条件

  1. JDK 17 与 Maven 已安装(安装步骤见《Ubuntu 22.04 安装 Apache Commons Geometry 指南》)。
  2. 网络可达中央仓库(首次构建需下载依赖)。
  3. 工程目录已创建:
mkdir -p /opt/geo-demo/src/main/java/demo
cd /opt/geo-demo

三、依赖引入

在 pom.xml 声明 commons-geometry-euclidean:1.0,其余传递依赖由 Maven 自动解析:

<properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-geometry-euclidean</artifactId>
        <version>1.0</version>
    </dependency>
</dependencies>

验证依赖解析:

cd /opt/geo-demo
mvn -B dependency:tree

输出节选(实测首尾):

[INFO] demo:geometry-demo:jar:1.0.0
[INFO] \- org.apache.commons:commons-geometry-euclidean:jar:1.0:compile
[INFO]    +- org.apache.commons:commons-geometry-core:jar:1.0:compile
[INFO]    +- org.apache.commons:commons-numbers-core:jar:1.0:compile
[INFO]    +- org.apache.commons:commons-numbers-arrays:jar:1.0:compile
[INFO]    +- org.apache.commons:commons-numbers-angle:jar:1.0:compile
[INFO]    \- org.apache.commons:commons-numbers-quaternion:jar:1.0:compile
[INFO] BUILD SUCCESS

生成依赖 classpath 文件(供 java 直跑):

mvn -q -B dependency:build-classpath -Dmdep.outputFile=cp.txt

四、核心 API 速览

维度核心类型工厂/入口典型能力
2DVector2DVector2D.of(x, y)加减、点积、模长、归一化、距离
2DLine / SegmentLines.fromPoints(p1, p2, precision)投影、垂距、相交、平行判定
2DConvexArea / RegionBSPTree2DConvexArea.convexPolygonFromVertices(...)面积、质心、包含判定、布尔运算
3DVector3DVector3D.of(x, y, z)加减、点积、叉积、距离
3DPlane / Line3DPlanes.fromPoints(...)、Lines3D.fromPoints(...)投影、偏移、相交、平行判定
精度Precision.DoubleEquivalencePrecision.doubleEquivalenceOfEpsilon(eps)容差比较,所有几何对象构造时传入

💡 技术拓展:几何计算库不止本文所用的 Apache Commons Geometry。常见选项对比——

库语言/形态特点适用场景
Commons GeometryJava 纯库Apache 出品、坐标/向量/直线/区域 BSP 类型丰富、无 JTS 依赖Java 服务内轻量几何运算
JTS Topology SuiteJava 纯库二维拓扑、空间关系与几何谓词最完备GIS 拓扑、空间分析
GeoToolsJava 库基于 JTS 的 GIS 工具集、坐标参考系统丰富地图数据与投影处理
ShapelyPython 库基于 GEOS,交互式脚本友好数据分析/脚本化几何处理

本文属于 Java 服务内几何计算场景,选用 Apache Commons Geometry:它不依赖 JTS、模块清晰、2D/3D 一体,且可通过 Maven 直接引入。

五、示例 1:2D 向量与直线运算

/opt/geo-demo/src/main/java/demo/Geometry2DDemo.java,覆盖向量运算、直线投影/垂距、两线相交、线段构造与多边形面积:

package demo;

import java.util.List;
import java.util.Arrays;

import org.apache.commons.geometry.euclidean.twod.ConvexArea;
import org.apache.commons.geometry.euclidean.twod.Line;
import org.apache.commons.geometry.euclidean.twod.Lines;
import org.apache.commons.geometry.euclidean.twod.Segment;
import org.apache.commons.geometry.euclidean.twod.Vector2D;
import org.apache.commons.numbers.core.Precision;

public class Geometry2DDemo {

    public static void main(String[] args) {
        final Precision.DoubleEquivalence precision = Precision.doubleEquivalenceOfEpsilon(1e-10);

        Vector2D a = Vector2D.of(3.0, 4.0);
        Vector2D b = Vector2D.of(1.0, 2.0);
        System.out.println("[v-add]   a.add(b)      = " + a.add(b));
        System.out.println("[v-sub]   a.subtract(b) = " + a.subtract(b));
        System.out.println("[v-dot]   a.dot(b)      = " + a.dot(b));
        System.out.println("[v-norm]  a.norm()      = " + a.norm());
        System.out.println("[v-unit]  a.normalize() = " + a.normalize());
        System.out.println("[pt-dist] a.distance(b) = " + a.distance(b));

        Line line = Lines.fromPoints(Vector2D.of(0.0, 0.0), Vector2D.of(4.0, 0.0), precision);
        Vector2D q = Vector2D.of(2.0, 3.0);
        System.out.println("[line]    fromPoints(0,0)-(4,0) = " + line);
        System.out.println("[proj]    line.project(2,3)   = " + line.project(q));
        System.out.println("[line-dis]line.distance(2,3)  = " + line.distance(q));

        Line l1 = Lines.fromPoints(Vector2D.of(0.0, 0.0), Vector2D.of(4.0, 4.0), precision);
        Line l2 = Lines.fromPoints(Vector2D.of(0.0, 4.0), Vector2D.of(4.0, 0.0), precision);
        System.out.println("[x-line]  l1.intersection(l2) = " + l1.intersection(l2));
        System.out.println("[parallel]l1.isParallel(l2)   = " + l1.isParallel(l2));

        Segment seg = Lines.segmentFromPoints(Vector2D.of(0.0, 0.0), Vector2D.of(3.0, 4.0), precision);
        System.out.println("[seg]     start/end          = " + seg.getStartPoint() + " / " + seg.getEndPoint());
        System.out.println("[seg-len] seg.getSize()       = " + seg.getSize());
        System.out.println("[seg-mid] seg.getCentroid()   = " + seg.getCentroid());
        System.out.println("[seg-has] seg.contains(1.5,2) = " + seg.contains(Vector2D.of(1.5, 2.0)));

        List<Vector2D> square = Arrays.asList(
                Vector2D.of(0.0, 0.0), Vector2D.of(4.0, 0.0),
                Vector2D.of(4.0, 3.0), Vector2D.of(0.0, 3.0));
        ConvexArea area = ConvexArea.convexPolygonFromVertices(square, precision);
        System.out.println("[area-convex] getSize()     = " + area.getSize());
        System.out.println("[area-centroid]             = " + area.getCentroid());
        System.out.println("[area-tree]   toTree.getSize= " + area.toTree().getSize());
        System.out.println("[area-verts]  getVertices() = " + area.getVertices());
    }
}

编译并运行:

cd /opt/geo-demo
mvn -q compile
java -Xmx256m -cp "target/classes:$(cat cp.txt)" demo.Geometry2DDemo

输出:

[v-add]   a.add(b)      = (4.0, 6.0)
[v-sub]   a.subtract(b) = (2.0, 2.0)
[v-dot]   a.dot(b)      = 11.0
[v-norm]  a.norm()      = 5.0
[v-unit]  a.normalize() = (0.6000000000000001, 0.8)
[pt-dist] a.distance(b) = 2.8284271247461903
[line]    fromPoints(0,0)-(4,0) = Line[origin= (0.0, 0.0), direction= (1.0, 0.0)]
[proj]    line.project(2,3)   = (2.0, 0.0)
[line-dis]line.distance(2,3)  = 3.0
[x-line]  l1.intersection(l2) = (2.0, 2.0)
[parallel]l1.isParallel(l2)   = false
[seg]     start/end          = (0.0, 0.0) / (3.0, 4.0)
[seg-len] seg.getSize()       = 5.0
[seg-mid] seg.getCentroid()   = (1.5, 2.0)
[seg-has] seg.contains(1.5,2) = true
[area-convex] getSize()     = 12.0
[area-centroid]             = (2.0, 1.5)
[area-tree]   toTree.getSize= 12.0
[area-verts]  getVertices() = [(0.0, 0.0), (4.0, 0.0), (4.0, 3.0), (0.0, 3.0)]

结果校验:(3,4)+(1,2)=(4,6)、点积 3*1+4*2=11、|(3,4)|=5;点 (2,3) 到 x 轴垂距 3.0;两对角线交于 (2,2);3×4 矩形面积 12.0、质心 (2,1.5)。全部与几何常识一致。

六、示例 2:3D 向量、平面与直线

/opt/geo-demo/src/main/java/demo/Geometry3DDemo.java,覆盖 3D 向量运算、三点定面、投影/偏移、直线与平面求交:

package demo;

import org.apache.commons.geometry.euclidean.threed.Plane;
import org.apache.commons.geometry.euclidean.threed.Planes;
import org.apache.commons.geometry.euclidean.threed.Vector3D;
import org.apache.commons.geometry.euclidean.threed.line.Line3D;
import org.apache.commons.geometry.euclidean.threed.line.Lines3D;
import org.apache.commons.numbers.core.Precision;

public class Geometry3DDemo {

    public static void main(String[] args) {
        final Precision.DoubleEquivalence precision = Precision.doubleEquivalenceOfEpsilon(1e-10);

        Vector3D u = Vector3D.of(1.0, 0.0, 0.0);
        Vector3D v = Vector3D.of(0.0, 1.0, 0.0);
        System.out.println("[v3-add]   u.add(v)   = " + u.add(v));
        System.out.println("[v3-dot]   u.dot(v)   = " + u.dot(v));
        System.out.println("[v3-cross] u.cross(v) = " + u.cross(v));
        System.out.println("[v3-norm]  u.norm()   = " + u.norm());
        System.out.println("[v3-dist]  u.distance(Vector3D.of(4,0,0)) = "
                + u.distance(Vector3D.of(4.0, 0.0, 0.0)));

        Plane plane = Planes.fromPoints(
                Vector3D.of(0.0, 0.0, 0.0),
                Vector3D.of(1.0, 0.0, 0.0),
                Vector3D.of(0.0, 1.0, 0.0), precision);
        System.out.println("[plane]    fromPoints        = " + plane);
        Vector3D pt = Vector3D.of(2.0, 3.0, 5.0);
        System.out.println("[proj]     plane.project(pt) = " + plane.project(pt));
        System.out.println("[offset]   plane.offset(pt)  = " + plane.offset(pt));

        Line3D line = Lines3D.fromPoints(
                Vector3D.of(2.0, 2.0, 5.0),
                Vector3D.of(2.0, 2.0, -5.0), precision);
        System.out.println("[line3d]   fromPoints        = " + line);
        System.out.println("[x]        plane.intersection(line) = " + plane.intersection(line));

        Plane wall = Planes.fromPointAndNormal(Vector3D.of(0.0, 0.0, 3.0), Vector3D.of(1.0, 0.0, 0.0), precision);
        System.out.println("[x-plane]  wall.intersection(plane) = " + wall.intersection(plane));
        Plane roof = Planes.fromPointAndNormal(Vector3D.of(0.0, 0.0, 3.0), Vector3D.of(0.0, 0.0, 1.0), precision);

        Plane high = Planes.fromPointAndNormal(Vector3D.of(0.0, 0.0, 7.0), Vector3D.of(0.0, 0.0, 1.0), precision);
        System.out.println("[p-parallel] roof.isParallel(high) = " + roof.isParallel(high));
        System.out.println("[p-dist]     roof.offset(high)     = " + roof.offset(high));
    }
}

编译并运行:

mvn -q compile
java -Xmx256m -cp "target/classes:$(cat cp.txt)" demo.Geometry3DDemo

输出:

[v3-add]   u.add(v)   = (1.0, 1.0, 0.0)
[v3-dot]   u.dot(v)   = 0.0
[v3-cross] u.cross(v) = (0.0, 0.0, 1.0)
[v3-norm]  u.norm()   = 1.0
[v3-dist]  u.distance(Vector3D.of(4,0,0)) = 3.0
[plane]    fromPoints        = Plane[origin= (0.0, 0.0, 0.0), normal= (0.0, 0.0, 1.0)]
[proj]     plane.project(pt) = (2.0, 3.0, 0.0)
[offset]   plane.offset(pt)  = 5.0
[line3d]   fromPoints        = Line3D[origin= (2.0, 2.0, 0.0), direction= (0.0, 0.0, -1.0)]
[x]        plane.intersection(line) = (2.0, 2.0, 0.0)
[x-plane]  wall.intersection(plane) = Line3D[origin= (0.0, 0.0, 0.0), direction= (0.0, -1.0, 0.0)]
[p-parallel] roof.isParallel(high) = true
[p-dist]     roof.offset(high)     = 4.0

结果校验:x 轴与 y 轴单位向量叉积为 z 轴单位向量;点 (2,3,5) 在 xy 平面投影 (2,3,0)、偏移 5.0;竖直线与 xy 平面交于 (2,2,0);两个相距 4 的平行平面 offset 为 4.0。

七、示例 3:区域与点定位(点是否在多边形内)

/opt/geo-demo/src/main/java/demo/ContainsDemo.java,把多边形转为 BSP 区域树,做包含判定与最近边界投影(适合配送围栏、碰撞检测等业务):

package demo;

import java.util.Arrays;
import org.apache.commons.geometry.euclidean.twod.ConvexArea;
import org.apache.commons.geometry.euclidean.twod.RegionBSPTree2D;
import org.apache.commons.geometry.euclidean.twod.Vector2D;
import org.apache.commons.numbers.core.Precision;

public class ContainsDemo {
    public static void main(String[] args) {
        Precision.DoubleEquivalence precision = Precision.doubleEquivalenceOfEpsilon(1e-10);
        ConvexArea square = ConvexArea.convexPolygonFromVertices(Arrays.asList(
                Vector2D.of(0.0, 0.0), Vector2D.of(4.0, 0.0),
                Vector2D.of(4.0, 4.0), Vector2D.of(0.0, 4.0)), precision);
        RegionBSPTree2D region = square.toTree();
        System.out.println("[inside]  contains(2,2)   = " + region.contains(Vector2D.of(2.0, 2.0)));
        System.out.println("[outside] contains(5,5)   = " + region.contains(Vector2D.of(5.0, 5.0)));
        System.out.println("[boundary]contains(2,4)   = " + region.contains(Vector2D.of(2.0, 4.0)));
        Vector2D q = Vector2D.of(5.0, 5.0);
        Vector2D closest = region.project(q);
        System.out.println("[closest] project(5,5)     = " + closest);
        System.out.println("[dist]    dist(5,5)->edge  = " + q.distance(closest));
    }
}

编译并运行:

mvn -q compile
java -Xmx256m -cp "target/classes:$(cat cp.txt)" demo.ContainsDemo

输出:

[inside]  contains(2,2)   = true
[outside] contains(5,5)   = false
[boundary]contains(2,4)   = true
[closest] project(5,5)     = (4.0, 4.0)
[dist]    dist(5,5)->edge  = 1.4142135623730951

结果校验:正方形 [0,4]x[0,4] 内点 (2,2) 判定在内、外点 (5,5) 判定在外、边界点 (2,4) 判定在边界上;(5,5) 投影到角点 (4,4),距离 √2≈1.414。

八、示例 4:精度与容差控制

/opt/geo-demo/src/main/java/demo/PrecisionDemo.java,演示同一对值在不同容差下的判定差异(详见《Apache Commons Geometry 配置参数详解》):

package demo;

import org.apache.commons.geometry.euclidean.twod.Line;
import org.apache.commons.geometry.euclidean.twod.Lines;
import org.apache.commons.geometry.euclidean.twod.Vector2D;
import org.apache.commons.numbers.core.Precision;

public class PrecisionDemo {

    public static void main(String[] args) {
        Precision.DoubleEquivalence strict = Precision.doubleEquivalenceOfEpsilon(1e-10);
        Precision.DoubleEquivalence loose = Precision.doubleEquivalenceOfEpsilon(1e-4);

        System.out.println("[const] Precision.EPSILON  = " + Precision.EPSILON);
        System.out.println("[const] Precision.SAFE_MIN = " + Precision.SAFE_MIN);
        System.out.println("[eps] eps=1e-10 eq(1.0,1.0) = " + strict.eq(1.0, 1.0)
                + " , eps=1e-4 eq(1.0,1.0) = " + loose.eq(1.0, 1.0));

        Vector2D p = Vector2D.of(1.0, 1.0);
        Vector2D near = Vector2D.of(1.0, 1.0 + 1e-5);
        System.out.println("[pt-eq-strict] 1,1 vs 1,1.00001 = " + p.eq(near, strict));
        System.out.println("[pt-eq-loose]  1,1 vs 1,1.00001 = " + p.eq(near, loose));

        Vector2D tiny = Vector2D.of(1e-5, 0.0);
        System.out.println("[zero-strict] eqZero(1e-5,0) = " + strict.eqZero(tiny.getX()));
        System.out.println("[zero-loose]  eqZero(1e-5,0) = " + loose.eqZero(tiny.getX()));

        Line base = Lines.fromPoints(Vector2D.of(0.0, 0.0), Vector2D.of(1.0, 0.0), loose);
        Line nearlyParallel = Lines.fromPoints(Vector2D.of(0.0, 1.0), Vector2D.of(1.0, 1.0 + 1e-5), loose);
        System.out.println("[parallel-loose]  isParallel = " + base.isParallel(nearlyParallel));

        Line baseStrict = Lines.fromPoints(Vector2D.of(0.0, 0.0), Vector2D.of(1.0, 0.0), strict);
        Line nearlyStrict = Lines.fromPoints(Vector2D.of(0.0, 1.0), Vector2D.of(1.0, 1.0 + 1e-5), strict);
        System.out.println("[parallel-strict] isParallel = " + baseStrict.isParallel(nearlyStrict));

        System.out.println("[side] offset(0,5) = " + base.offset(Vector2D.of(0.0, 5.0)));
        System.out.println("[side] offset(0,-5)= " + base.offset(Vector2D.of(0.0, -5.0)));
    }
}

编译并运行:

mvn -q compile
java -Xmx256m -cp "target/classes:$(cat cp.txt)" demo.PrecisionDemo

输出:

[const] Precision.EPSILON  = 1.1102230246251565E-16
[const] Precision.SAFE_MIN = 2.2250738585072014E-308
[eps] eps=1e-10 eq(1.0,1.0) = true , eps=1e-4 eq(1.0,1.0) = true
[pt-eq-strict] 1,1 vs 1,1.00001 = false
[pt-eq-loose]  1,1 vs 1,1.00001 = true
[zero-strict] eqZero(1e-5,0) = false
[zero-loose]  eqZero(1e-5,0) = true
[parallel-loose]  isParallel = true
[parallel-strict] isParallel = false
[side] offset(0,5) = -5.0
[side] offset(0,-5)= 5.0

结果校验:差 1e-5 的两点在容差 1e-10 下判定不等、在 1e-4 下判定相等;两条“几乎平行”的直线同理;offset 符号表示点在直线两侧。

九、集成到业务工程

Geometry 是进程内库,集成即“引入依赖 + 调用 API”,无服务化步骤。两种常见集成形态:

形态 1:作为工程依赖(Maven),业务代码直接调用(见第五~八章示例),与 Spring Boot/Quarkus 等框架无冲突,仅需保证 JDK 17 与编码 UTF-8。

形态 2:打包分发,产出 jar 并携带依赖:

cd /opt/geo-demo
mvn -B package
ls -l target/geometry-demo-1.0.0.jar

输出:

-rw-r--r-- 1 root root 11609 Oct 13 18:08 target/geometry-demo-1.0.0.jar

分发时把工程 jar 与 cp.txt 对应的 6 个依赖 jar 一起部署,目标机运行:

java -Xmx256m -cp "geometry-demo-1.0.0.jar:$(cat cp.txt)" demo.Geometry2DDemo

十、注意事项

  1. 精度参数必传:所有 Lines.*、Planes.*、ConvexArea.* 工厂方法都需要 Precision.DoubleEquivalence,忘传会编译报错;容差选择见《Apache Commons Geometry 配置参数详解》。
  2. 堆内存:几何计算对象量小时 -Xmx256m 足够;批量区域运算可放宽到 512 MB。
  3. 编码:源码含中文注释时必须 UTF-8(pom 已设 project.build.sourceEncoding,手动 javac 需 -encoding UTF-8)。
  4. 模块选择:2D/3D 用 commons-geometry-euclidean;球面用 commons-geometry-spherical;IO 读写用 commons-geometry-io-euclidean(均为 1.0)。

十一、常见问题(FAQ)

Q1:cannot find symbol: Lines.fromPoints?

确认引入 org.apache.commons.geometry.euclidean.twod.Lines(工厂类,复数形式),不是 Line。

Q2:RegionBSPTree2D 没有 boundaryDistance 方法?

1.0 中距离需通过 project(pt) 取最近点再自行求距(见第七章示例);contains、intersection、union 等布尔运算直接可用。

Q3:java 运行报 NoClassDefFoundError: Precision?

classpath 缺 commons-numbers-core。用 mvn dependency:build-classpath 生成的 cp.txt 拼入 -cp,或补齐 5 个依赖 jar。

Q4:结果精度不对,差 1e-6 就算不相等?

容差 eps 传得太小。按坐标量级选容差:小坐标 1e-10,大坐标 1e-6 或更大(见《Apache Commons Geometry 配置参数详解》5.5 节)。

Q5:mvn exec:java 运行的是别的类?

pom.xml 的 exec-maven-plugin 配置优先,命令行 -Dexec.mainClass 不覆盖;要换主类用 java -cp 直跑。

十二、参考链接

  1. Apache Commons Geometry 官方站点:https://commons.apache.org/proper/commons-geometry/
  2. Apache Commons Geometry 用户指南:https://commons.apache.org/proper/commons-geometry/userguide/index.html
  3. Maven 中央仓库坐标页:https://repo1.maven.org/maven2/org/apache/commons/commons-geometry-euclidean/
  4. Commons Numbers Precision API:https://commons.apache.org/proper/commons-numbers/javadocs/api-release/org/apache/commons/numbers/core/Precision.html