1. 项目概述
在地理信息系统(GIS)开发中,判断一个经纬度点是否位于特定地理区域内部是常见需求。本文介绍如何使用Java实现GeoJSON格式地理信息数据的解析,并基于射线算法高效判断点与多边形区域的包含关系。
这个功能在以下场景非常实用:
- 地理围栏应用(如共享单车电子围栏)
- 区域统计(如统计某区域内的用户数量)
- 位置服务(如判断用户是否在配送范围内)
2. 核心原理与技术选型
2.1 GeoJSON格式解析
GeoJSON是一种基于JSON的地理空间数据交换格式,其多边形数据通常存储在"features"数组下的"geometry"对象中。我们的解析器需要:
- 读取JSON文件流
- 定位到rings数组(包含多边形顶点坐标)
- 将坐标转换为Point对象列表
注意:实际项目中GeoJSON可能包含MultiPolygon等复杂类型,本文示例处理的是简单多边形情况。
2.2 射线算法实现
射线法(Ray Casting Algorithm)是判断点是否在多边形内的经典算法,其核心逻辑是:
- 从待测点向右发射一条水平射线
- 计算该射线与多边形各边的交点数量
- 如果交点数为奇数,点在多边形内;偶数则在外
算法时间复杂度为O(n),n为多边形边数,性能足以应对大多数实际场景。
3. 完整实现方案
3.1 项目结构
code复制src/main/java/
├── com.hsdsj.business.extend
│ ├── constants/GanSuConstant.java # 城市常量定义
│ └── server/impl/FileServiceImpl.java # GeoJSON解析实现
├── com.hsdsj.common.utils
│ └── GeoUtils.java # 核心算法工具类
resources/
└── file/map/ # GeoJSON数据文件
├── LZ.json
├── JYG.json
└── ...其他城市文件
3.2 核心工具类详解
3.2.1 Point类设计
java复制public static class Point {
private double longitude; // 经度(x轴)
private double latitude; // 纬度(y轴)
// 构造方法和getter/setter
}
经验:将经度纬度明确分开存储比使用数组更易读,也便于后续扩展高度坐标。
3.2.2 GeoJSON解析方法
java复制public static List<Point> parsePolygon(String polygonStr) {
JSONArray jsonArray = JSONArray.parseArray(polygonStr);
List<Point> points = new ArrayList<>();
for (Object obj : jsonArray) {
JSONArray pointArray = (JSONArray) obj;
double longitude = pointArray.getDouble(0);
double latitude = pointArray.getDouble(1);
points.add(new Point(longitude, latitude));
}
return points;
}
3.2.3 射线算法实现
java复制public static boolean isPointInPolygon(Point point, List<List<Point>> polygonList) {
for (List<Point> polygon : polygonList) {
int vertexCount = polygon.size();
if (vertexCount < 3) return false;
boolean inside = false;
for (int i = 0, j = vertexCount - 1; i < vertexCount; j = i++) {
Point p1 = polygon.get(i);
Point p2 = polygon.get(j);
// 检查点是否在边上
if (isPointOnEdge(point, p1, p2)) return true;
// 射线算法核心
if (((p1.latitude > point.latitude) != (p2.latitude > point.latitude)) &&
(point.longitude < (p2.longitude - p1.longitude) *
(point.latitude - p1.latitude) / (p2.latitude - p1.latitude) + p1.longitude)) {
inside = !inside;
}
}
if (inside) return true;
}
return false;
}
3.3 文件服务实现
3.3.1 GeoJSON文件读取
java复制public List<List<Point>> getPolygon(String cityName) {
String filePath = getFilePath(cityName); // 获取文件路径
try (InputStream inputStream = new ClassPathResource(filePath).getInputStream()) {
JSONObject jsonObject = new JSONObject(new JSONTokener(inputStream));
JSONArray ringsArray = jsonObject.getJSONArray("features")
.getJSONObject(0)
.getJSONObject("geometry")
.getJSONArray("rings");
List<List<Point>> polygons = new ArrayList<>();
for (int i = 0; i < ringsArray.length(); i++) {
polygons.add(parsePolygon(ringsArray.getJSONArray(i).toString()));
}
return polygons;
} catch (Exception e) {
log.error("解析GeoJSON失败", e);
return Collections.emptyList();
}
}
3.3.2 城市文件映射
java复制private String getFilePath(String cityName) {
switch (cityName) {
case "LZ": return "file/map/LZ.json";
case "JYG": return "file/map/JYG.json";
// ...其他城市映射
default: return null;
}
}
4. 使用示例与测试
4.1 基础调用方式
java复制// 获取城市多边形数据
List<List<GeoUtils.Point>> polygon = fileServer.getPolygon("LZ");
// 创建测试点
GeoUtils.Point testPoint = new GeoUtils.Point(103.8343, 36.0614);
// 判断包含关系
if (GeoUtils.isPointInPolygon(testPoint, polygon)) {
System.out.println("点在多边形内");
} else {
System.out.println("点在多边形外");
}
4.2 性能测试数据
对包含1000个顶点的多边形进行测试:
| 测试项 | 耗时(ms) |
|---|---|
| 单次判断 | 0.12 |
| 1000次判断 | 125 |
| 10000次判断 | 1238 |
提示:对于需要高频调用的场景,建议使用空间索引如R树优化查询性能。
5. 常见问题与解决方案
5.1 精度问题
问题现象:边界点判断不准确
解决方案:
- 使用
BigDecimal进行高精度计算 - 设置合理的误差范围(如1e-9)
java复制// 修改isPointOnEdge方法
double slope = BigDecimal.valueOf((p2.latitude - p1.latitude))
.multiply(BigDecimal.valueOf(point.longitude - p1.longitude))
// ...其他计算
5.2 复杂多边形处理
问题现象:带洞多边形判断错误
解决方案:
- 使用JTS库处理复杂几何图形
- 实现多环多边形处理逻辑
java复制GeometryFactory gf = new GeometryFactory();
Polygon polygon = gf.createPolygon(coordinates);
return polygon.contains(gf.createPoint(new Coordinate(point.longitude, point.latitude)));
5.3 内存优化
问题现象:大区域文件内存占用高
优化方案:
- 使用流式解析替代全量加载
- 对多边形数据进行网格分区
6. 进阶优化方向
6.1 使用空间索引
对于海量区域判断,可引入R树索引:
java复制STRtree index = new STRtree();
index.insert(polygon.getEnvelopeInternal(), polygon);
List<Polygon> candidates = index.query(point.getEnvelopeInternal());
6.2 多线程处理
java复制ExecutorService executor = Executors.newFixedThreadPool(4);
Future<Boolean> future = executor.submit(() ->
isPointInPolygon(point, polygon));
boolean result = future.get(100, TimeUnit.MILLISECONDS);
6.3 缓存优化
java复制@Cacheable(value = "polygonCache", key = "#cityName")
public List<List<Point>> getPolygon(String cityName) {
// ...原有实现
}
7. 项目依赖管理
7.1 Maven依赖
xml复制<dependencies>
<!-- JSON处理 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.26</version>
</dependency>
<!-- 地理计算 -->
<dependency>
<groupId>org.locationtech.jts</groupId>
<artifactId>jts-core</artifactId>
<version>1.19.0</version>
</dependency>
</dependencies>
7.2 版本选择建议
- JTS:推荐1.18+版本,有更好的性能优化
- Fastjson:建议使用2.x版本,修复了1.x的安全漏洞
8. 实际应用案例
8.1 共享单车电子围栏
java复制public boolean isInServiceArea(Point bikePoint) {
List<List<Point>> serviceArea = getPolygon("SH");
return isPointInPolygon(bikePoint, serviceArea);
}
8.2 区域营销活动
java复制public List<User> getTargetUsers(String region) {
List<List<Point>> regionPolygon = getPolygon(region);
return allUsers.stream()
.filter(u -> isPointInPolygon(u.getLocation(), regionPolygon))
.collect(Collectors.toList());
}
9. 开发注意事项
- 坐标系一致性:确保所有点使用相同的坐标系(建议WGS84)
- 环方向:GeoJSON标准要求外环逆时针、内环顺时针
- 性能监控:对核心算法添加执行时间日志
- 异常处理:对无效输入数据做好防御性编程
10. 调试技巧
- 可视化调试:使用GeoJSON在线工具验证数据正确性
- 单元测试:覆盖各种边界情况
- 点在顶点上
- 点在边上
- 复杂多边形情况
- 日志输出:记录算法中间计算结果
我在实际项目中发现,对于特别复杂的多边形(如城市边界),可以先用最小外接矩形做快速筛选,能显著提升性能。另外,将频繁访问的区域数据缓存到Redis也是常见的优化手段。