JTS简单使用
前言
LocationTech JTS 拓扑套件(JTS)是一个开源的 Java 软件库,提供了平面几何的对象模型以及一套基础的几何函数。JTS 遵循开放 GIS 联盟发布的 SQL 简单要素规范(Simple Features Specification for SQL)。JTS旨在作为基于矢量的地理信息软件(如地理信息系统)的核心组件使用。它还可以作为一个通用库,提供计算几何学中的算法。
- WGS-84坐标系 : 国际标准,真实坐标。全球定位系统直接获取的就是WGS-84坐标。
- GCJ-02坐标系 : 国家加密,偏移坐标。通过一个特殊的非线性算法,对WGS-84的真实坐标进行了加密偏移,偏移量从几百米到上千米不等。
- WKT(Well-Known Text):是一种用于表示地理空间几何对象的文本标记语言,在 GIS(地理信息系统)、数据库(如 PostGIS、MySQL、SQL Server)和各类地图开发中广泛使用。点 POINT(116.397428 39.909204)
- Geohash :是一种地理编码系统,将二维的经纬度坐标转换为一维的字符串编码。它是一种分级递推的空间数据结构,广泛应用于地理信息系统、数据库索引和位置服务中。
maven依赖
<dependency>
<groupId>org.locationtech.jts</groupId>
<artifactId>jts-core</artifactId>
<version>1.18.2</version>
</dependency>
代码
坐标距离计算工具类
package com.imooc.sourcecode2.jts;
import lombok.extern.slf4j.Slf4j;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.io.ParseException;
import org.locationtech.jts.io.WKTReader;
/**
* 坐标距离计算工具类
* 提供在不同坐标系下计算两点之间距离的方法,以及点到多边形的距离计算
* 支持高德坐标系(GCJ-02)和WGS84坐标系
*/
@Slf4j
public class CoordDistanceUtils {
/**
* 地球平均半径(米)
*/
private static final double EARTH_RADIUS = 6371000;
/**
* 几何图形工厂,线程安全,可以共享使用
*/
private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory();
/**
* WKTReader线程本地变量,确保线程安全
*/
private static final ThreadLocal<WKTReader> WKT_READER_THREAD_LOCAL =
ThreadLocal.withInitial(() -> new WKTReader(GEOMETRY_FACTORY));
/**
* 计算两个WGS84坐标点之间的距离(米)
* 使用Haversine公式计算球面距离
*
* @param lat1 第一个点的纬度
* @param lng1 第一个点的经度
* @param lat2 第二个点的纬度
* @param lng2 第二个点的经度
* @return 两点之间的距离,单位:米
*/
public static double calculateDistanceInWgs84(double lat1, double lng1, double lat2, double lng2) {
// 转换为弧度
double radLat1 = Math.toRadians(lat1);
double radLng1 = Math.toRadians(lng1);
double radLat2 = Math.toRadians(lat2);
double radLng2 = Math.toRadians(lng2);
// 计算经纬度差值
double deltaLat = radLat2 - radLat1;
double deltaLng = radLng2 - radLng1;
// Haversine公式
double a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
Math.cos(radLat1) * Math.cos(radLat2) *
Math.sin(deltaLng / 2) * Math.sin(deltaLng / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
// 计算距离
return EARTH_RADIUS * c;
}
/**
* 计算两个高德坐标系(GCJ-02)点之间的距离(米)
* 先将高德坐标转换为WGS84坐标,再计算距离
*
* @param lat1 第一个点的纬度(高德坐标系)
* @param lng1 第一个点的经度(高德坐标系)
* @param lat2 第二个点的纬度(高德坐标系)
* @param lng2 第二个点的经度(高德坐标系)
* @return 两点之间的距离,单位:米
*/
public static double calculateDistanceInGcj02(double lat1, double lng1, double lat2, double lng2) {
// 将高德坐标转换为WGS84坐标
double[] wgsCoord1 = CoordTransUtils.gcj02ToWgs84(lat1, lng1);
double[] wgsCoord2 = CoordTransUtils.gcj02ToWgs84(lat2, lng2);
// 计算WGS84坐标下的距离
return calculateDistanceInWgs84(wgsCoord1[0], wgsCoord1[1], wgsCoord2[0], wgsCoord2[1]);
}
/**
* 计算点到WKT多边形的最短距离(米)
* 如果点在多边形内部,返回0
*
* @param lat 点的纬度(WGS84坐标系)
* @param lng 点的经度(WGS84坐标系)
* @param wkt 多边形的WKT表示(WGS84坐标系)
* @return 点到多边形的最短距离,单位:米
*/
public static double calculateDistanceToPolygonInWgs84(double lat, double lng, String wkt) {
try {
// 创建点
Point point = GEOMETRY_FACTORY.createPoint(new Coordinate(lng, lat));
// 解析多边形
WKTReader reader = WKT_READER_THREAD_LOCAL.get();
Geometry polygon = reader.read(wkt);
// 计算距离(JTS返回的是度数单位的距离)
double degreeDistance = polygon.distance(point);
// 如果点在多边形内部,距离为0
if (degreeDistance == 0) {
return 0;
}
// 将度数距离转换为米
// 这是一个近似计算,在小距离内较为准确
// 纬度1度约等于111km,经度1度随纬度变化
double latDegreeToMeter = 111000; // 纬度1度约等于111km
double lngDegreeToMeter = 111000 * Math.cos(Math.toRadians(lat)); // 经度1度随纬度变化
// 找到多边形上距离点最近的点
Coordinate nearestPoint = polygon.getFactory().createPoint(polygon.getCoordinate()).getCoordinate();
for (Coordinate coord : polygon.getCoordinates()) {
Point tempPoint = GEOMETRY_FACTORY.createPoint(coord);
if (point.distance(tempPoint) < point.distance(GEOMETRY_FACTORY.createPoint(nearestPoint))) {
nearestPoint = coord;
}
}
// 计算点到最近点的距离
return calculateDistanceInWgs84(lat, lng, nearestPoint.y, nearestPoint.x);
} catch (ParseException e) {
log.error("解析WKT字符串失败: {}", wkt, e);
throw new IllegalArgumentException("无效的WKT格式: " + wkt, e);
}
}
/**
* 计算点到WKT多边形的最短距离(米)
* 如果点在多边形内部,返回0
* 注意:输入的坐标和WKT均为高德坐标系(GCJ-02)
*
* @param lat 点的纬度(高德坐标系)
* @param lng 点的经度(高德坐标系)
* @param wkt 多边形的WKT表示(高德坐标系)
* @return 点到多边形的最短距离,单位:米
*/
public static double calculateDistanceToPolygonInGcj02(double lat, double lng, String wkt) {
// 将高德坐标转换为WGS84坐标
double[] wgsCoord = CoordTransUtils.gcj02ToWgs84(lat, lng);
// 将高德坐标系WKT转换为WGS84坐标系WKT
String wgs84Wkt = CoordTransUtils.gcjWktToWgsWkt(wkt);
// 计算WGS84坐标下的距离
return calculateDistanceToPolygonInWgs84(wgsCoord[0], wgsCoord[1], wgs84Wkt);
}
/**
* 判断两个点之间的距离是否小于指定阈值
*
* @param lat1 第一个点的纬度(高德坐标系)
* @param lng1 第一个点的经度(高德坐标系)
* @param lat2 第二个点的纬度(高德坐标系)
* @param lng2 第二个点的经度(高德坐标系)
* @param thresholdMeters 阈值距离,单位:米
* @return 如果距离小于阈值,返回true;否则返回false
*/
public static boolean isWithinDistance(double lat1, double lng1, double lat2, double lng2, double thresholdMeters) {
double distance = calculateDistanceInGcj02(lat1, lng1, lat2, lng2);
return distance <= thresholdMeters;
}
/**
* 判断点是否在多边形的指定距离范围内
* 如果点在多边形内部或距离小于指定阈值,返回true
*
* @param lat 点的纬度(高德坐标系)
* @param lng 点的经度(高德坐标系)
* @param wkt 多边形的WKT表示(高德坐标系)
* @param thresholdMeters 阈值距离,单位:米
* @return 如果点在多边形内部或距离小于阈值,返回true;否则返回false
*/
public static boolean isWithinDistanceOfPolygon(double lat, double lng, String wkt, double thresholdMeters) {
double distance = calculateDistanceToPolygonInGcj02(lat, lng, wkt);
return distance <= thresholdMeters;
}
}
坐标系转换工具类
package com.imooc.sourcecode2.jts;
import lombok.extern.slf4j.Slf4j;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.util.GeometryEditor;
import org.locationtech.jts.io.ParseException;
import org.locationtech.jts.io.WKTReader;
import org.locationtech.jts.io.WKTWriter;
/**
* 坐标系转换工具类
* 提供在高德坐标系(GCJ-02)和WGS84坐标系之间的转换功能
* GCJ-02: 中国国测局坐标系,高德、腾讯等国内地图使用
* WGS84: 国际标准坐标系,GPS和Google Earth使用
*/
@Slf4j
public class CoordTransUtils {
/**
* 地球半径,单位:米
*/
private static final double EARTH_RADIUS = 6378245.0;
/**
* 坐标系偏移参数
*/
private static final double EE = 0.00669342162296594323;
/**
* 几何图形工厂,线程安全,可以共享使用
*/
private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory();
/**
* WKTReader线程本地变量,确保线程安全
*/
private static final ThreadLocal<WKTReader> WKT_READER_THREAD_LOCAL =
ThreadLocal.withInitial(() -> new WKTReader(GEOMETRY_FACTORY));
/**
* WKTWriter线程本地变量,确保线程安全
*/
private static final ThreadLocal<WKTWriter> WKT_WRITER_THREAD_LOCAL =
ThreadLocal.withInitial(WKTWriter::new);
/**
* 将高德坐标系(GCJ-02)转换为WGS84坐标系
*
* @param lat 高德坐标系的纬度
* @param lng 高德坐标系的经度
* @return WGS84坐标系的坐标,[纬度, 经度]
*/
public static double[] gcj02ToWgs84(double lat, double lng) {
if (isOutOfChina(lat, lng)) {
return new double[]{lat, lng};
}
double dLat = transformLat(lng - 105.0, lat - 35.0);
double dLng = transformLng(lng - 105.0, lat - 35.0);
double radLat = lat / 180.0 * Math.PI;
double magic = Math.sin(radLat);
magic = 1 - EE * magic * magic;
double sqrtMagic = Math.sqrt(magic);
dLat = (dLat * 180.0) / ((EARTH_RADIUS * (1 - EE)) / (magic * sqrtMagic) * Math.PI);
dLng = (dLng * 180.0) / (EARTH_RADIUS / sqrtMagic * Math.cos(radLat) * Math.PI);
double wgsLat = lat - dLat;
double wgsLng = lng - dLng;
return new double[]{wgsLat, wgsLng};
}
/**
* 将WGS84坐标系转换为高德坐标系(GCJ-02)
*
* @param lat WGS84坐标系的纬度
* @param lng WGS84坐标系的经度
* @return 高德坐标系的坐标,[纬度, 经度]
*/
public static double[] wgs84ToGcj02(double lat, double lng) {
if (isOutOfChina(lat, lng)) {
return new double[]{lat, lng};
}
double dLat = transformLat(lng - 105.0, lat - 35.0);
double dLng = transformLng(lng - 105.0, lat - 35.0);
double radLat = lat / 180.0 * Math.PI;
double magic = Math.sin(radLat);
magic = 1 - EE * magic * magic;
double sqrtMagic = Math.sqrt(magic);
dLat = (dLat * 180.0) / ((EARTH_RADIUS * (1 - EE)) / (magic * sqrtMagic) * Math.PI);
dLng = (dLng * 180.0) / (EARTH_RADIUS / sqrtMagic * Math.cos(radLat) * Math.PI);
double gcjLat = lat + dLat;
double gcjLng = lng + dLng;
return new double[]{gcjLat, gcjLng};
}
/**
* 将高德坐标系(GCJ-02)的WKT字符串转换为WGS84坐标系的WKT字符串
*
* @param gcjWkt 高德坐标系的WKT字符串
* @return WGS84坐标系的WKT字符串
*/
public static String gcjWktToWgsWkt(String gcjWkt) {
try {
// 解析WKT字符串为几何对象
WKTReader reader = WKT_READER_THREAD_LOCAL.get();
Geometry geometry = reader.read(gcjWkt);
// 转换几何对象中的所有坐标
Geometry transformedGeometry = transformGeometry(geometry, true);
// 将转换后的几何对象转换回WKT字符串
WKTWriter writer = WKT_WRITER_THREAD_LOCAL.get();
return writer.write(transformedGeometry);
} catch (ParseException e) {
log.error("解析WKT字符串失败: {}", gcjWkt, e);
throw new IllegalArgumentException("无效的WKT格式: " + gcjWkt, e);
}
}
/**
* 将WGS84坐标系的WKT字符串转换为高德坐标系(GCJ-02)的WKT字符串
*
* @param wgsWkt WGS84坐标系的WKT字符串
* @return 高德坐标系的WKT字符串
*/
public static String wgsWktToGcjWkt(String wgsWkt) {
try {
// 解析WKT字符串为几何对象
WKTReader reader = WKT_READER_THREAD_LOCAL.get();
Geometry geometry = reader.read(wgsWkt);
// 转换几何对象中的所有坐标
Geometry transformedGeometry = transformGeometry(geometry, false);
// 将转换后的几何对象转换回WKT字符串
WKTWriter writer = WKT_WRITER_THREAD_LOCAL.get();
return writer.write(transformedGeometry);
} catch (ParseException e) {
log.error("解析WKT字符串失败: {}", wgsWkt, e);
throw new IllegalArgumentException("无效的WKT格式: " + wgsWkt, e);
}
}
/**
* 将高德坐标系(GCJ-02)的几何对象转换为WGS84坐标系的几何对象
*
* @param gcjGeometry 高德坐标系的几何对象
* @return WGS84坐标系的几何对象
*/
public static Geometry gcjGeometryToWgsGeometry(Geometry gcjGeometry) {
return transformGeometry(gcjGeometry, true);
}
/**
* 将WGS84坐标系的几何对象转换为高德坐标系(GCJ-02)的几何对象
*
* @param wgsGeometry WGS84坐标系的几何对象
* @return 高德坐标系的几何对象
*/
public static Geometry wgsGeometryToGcjGeometry(Geometry wgsGeometry) {
return transformGeometry(wgsGeometry, false);
}
/**
* 将高德坐标系(GCJ-02)的WKT字符串解析并转换为WGS84坐标系的几何对象
*
* @param gcjWkt 高德坐标系的WKT字符串
* @return WGS84坐标系的几何对象
* @throws IllegalArgumentException 当WKT格式无效时抛出
*/
public static Geometry gcjWktToWgsGeometry(String gcjWkt) {
try {
// 解析WKT字符串为几何对象
WKTReader reader = WKT_READER_THREAD_LOCAL.get();
Geometry geometry = reader.read(gcjWkt);
// 转换几何对象中的所有坐标
return transformGeometry(geometry, true);
} catch (ParseException e) {
log.error("解析WKT字符串失败: {}", gcjWkt, e);
throw new IllegalArgumentException("无效的WKT格式: " + gcjWkt, e);
}
}
/**
* 将WGS84坐标系的WKT字符串解析并转换为高德坐标系(GCJ-02)的几何对象
*
* @param wgsWkt WGS84坐标系的WKT字符串
* @return 高德坐标系的几何对象
* @throws IllegalArgumentException 当WKT格式无效时抛出
*/
public static Geometry wgsWktToGcjGeometry(String wgsWkt) {
try {
// 解析WKT字符串为几何对象
WKTReader reader = WKT_READER_THREAD_LOCAL.get();
Geometry geometry = reader.read(wgsWkt);
// 转换几何对象中的所有坐标
return transformGeometry(geometry, false);
} catch (ParseException e) {
log.error("解析WKT字符串失败: {}", wgsWkt, e);
throw new IllegalArgumentException("无效的WKT格式: " + wgsWkt, e);
}
}
/**
* 转换几何对象中的所有坐标
* 使用GeometryEditor简化实现
*
* @param geometry 原始几何对象
* @param isGcjToWgs 是否从GCJ-02转换到WGS84,true表示GCJ-02到WGS84,false表示WGS84到GCJ-02
* @return 转换后的几何对象
*/
private static Geometry transformGeometry(Geometry geometry, boolean isGcjToWgs) {
// 使用GeometryEditor修改几何对象的坐标
GeometryEditor editor = new GeometryEditor(GEOMETRY_FACTORY);
// 使用匿名内部类实现GeometryEditor.CoordinateOperation接口
return editor.edit(geometry, new GeometryEditor.CoordinateOperation() {
@Override
public Coordinate[] edit(Coordinate[] coordinates, Geometry geometry) {
Coordinate[] result = new Coordinate[coordinates.length];
for (int i = 0; i < coordinates.length; i++) {
Coordinate coord = coordinates[i];
double[] transformed;
if (isGcjToWgs) {
transformed = gcj02ToWgs84(coord.y, coord.x);
} else {
transformed = wgs84ToGcj02(coord.y, coord.x);
}
result[i] = new Coordinate(transformed[1], transformed[0], coord.z);
}
return result;
}
});
}
/**
* 判断坐标是否在中国范围外
*
* @param lat 纬度
* @param lng 经度
* @return 是否在中国范围外
*/
private static boolean isOutOfChina(double lat, double lng) {
if (lng < 72.004 || lng > 137.8347) {
return true;
}
if (lat < 0.8293 || lat > 55.8271) {
return true;
}
return false;
}
/**
* 转换纬度
*/
private static double transformLat(double x, double y) {
double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(y * Math.PI) + 40.0 * Math.sin(y / 3.0 * Math.PI)) * 2.0 / 3.0;
ret += (160.0 * Math.sin(y / 12.0 * Math.PI) + 320 * Math.sin(y * Math.PI / 30.0)) * 2.0 / 3.0;
return ret;
}
/**
* 转换经度
*/
private static double transformLng(double x, double y) {
double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
ret += (20.0 * Math.sin(6.0 * x * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(x * Math.PI) + 40.0 * Math.sin(x / 3.0 * Math.PI)) * 2.0 / 3.0;
ret += (150.0 * Math.sin(x / 12.0 * Math.PI) + 300.0 * Math.sin(x / 30.0 * Math.PI)) * 2.0 / 3.0;
return ret;
}
}
GeoHash工具类,用于处理地理空间数据的编码和多边形计算
package com.imooc.sourcecode2.jts;
import ch.hsr.geohash.BoundingBox;
import ch.hsr.geohash.GeoHash;
import lombok.extern.slf4j.Slf4j;
import org.locationtech.jts.geom.*;
import org.locationtech.jts.io.ParseException;
import org.locationtech.jts.io.WKTReader;
import java.util.*;
/**
* GeoHash工具类,用于处理地理空间数据的编码和多边形计算
* Geohash精度说明(1-12):
* 精度 | 字符长度 | 覆盖范围 | 典型用途
* -----------------------------------------------
* 1 | 1 | ~5000 km | 大洲/国家级别
* 2 | 2 | ~1250 km | 国家/大区域
* 3 | 3 | ~156 km | 省级/大城市
* 4 | 4 | ~39 km | 城市级
* 5 | 5 | ~5 km | 区县/城镇
* 6 | 6 | ~1.2 km | 街道/社区
* 7 | 7 | ~150 m | 地标建筑
* 8 | 8 | ~19 m | 小型区域
* 9 | 9 | ~3.7 m | 建筑物轮廓
* 10 | 10 | ~0.5 m | 停车位/房间
* 11 | 11 | ~6 cm | 高精度测绘
* 12 | 12 | ~3.7 cm | 特殊定位需求
* 注:覆盖范围为纬度方向近似值,精度随纬度变化略有差异。
*
* 注意:本工具类默认输入的坐标为高德坐标系(GCJ-02),内部会自动转换为WGS84坐标系进行计算
*/
@Slf4j
public class GeoHashUtils {
/**
* 几何图形工厂,线程安全,可以共享使用
*/
private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory();
/**
* WKTReader线程本地变量,确保线程安全
*/
private static final ThreadLocal<WKTReader> WKT_READER_THREAD_LOCAL =
ThreadLocal.withInitial(() -> new WKTReader(GEOMETRY_FACTORY));
/**
* 纬度极值:正常计算GeoHash的纬度上限
*/
private static final double MAX_EXTREME_LATITUDE = 85.0;
/**
* 纬度极值:正常计算GeoHash的纬度下限
*/
private static final double MIN_EXTREME_LATITUDE = -85.0;
/**
* GeoHash计算时行的最大安全限制,防止死循环
*/
private static final int MAX_GEOHASH_ROWS = 1000;
/**
* GeoHash计算时列的最大安全限制,防止死循环
*/
private static final int MAX_GEOHASH_COLS = 1000;
/**
* 计算WKT多边形覆盖的GeoHash列表
* 注意:输入的WKT为高德坐标系(GCJ-02),内部会自动转换为WGS84坐标系进行计算
*
* @param wkt WKT格式的多边形字符串(高德坐标系)
* @param precision GeoHash精度,取值范围1-12,数字越大精度越高
* @return 与多边形相交的GeoHash集合
* @throws IllegalArgumentException 当WKT格式无效时抛出
*/
public static Set<String> getIntersectingGeoHashes(String wkt, int precision) {
if (wkt == null || wkt.isEmpty()) {
return Collections.emptySet();
}
validatePrecision(precision);
try {
// 直接获取WGS84坐标系的几何对象,避免不必要的WKT转换
Geometry geometry = CoordTransUtils.gcjWktToWgsGeometry(wkt);
return getIntersectingGeoHashesForGeometry(geometry, precision);
} catch (IllegalArgumentException e) {
log.error("Invalid WKT format: {}", wkt, e);
throw e;
}
}
/**
* 判断指定坐标是否在WKT多边形内部
* 注意:输入的坐标和WKT均为高德坐标系(GCJ-02),内部会自动转换为WGS84坐标系进行计算
*
* @param lat 纬度(高德坐标系)
* @param lon 经度(高德坐标系)
* @param wkt WKT格式的多边形字符串(高德坐标系)
* @return true表示点在多边形内,false表示点在多边形外
* @throws IllegalArgumentException 当WKT格式无效时抛出
*/
public static boolean isPointInPolygon(double lat, double lon, String wkt) {
try {
// 将高德坐标系转换为WGS84坐标系
double[] wgsCoord = CoordTransUtils.gcj02ToWgs84(lat, lon);
// 直接获取WGS84坐标系的几何对象,避免不必要的WKT转换
Geometry geometry = CoordTransUtils.gcjWktToWgsGeometry(wkt);
Point point = GEOMETRY_FACTORY.createPoint(new Coordinate(wgsCoord[1], wgsCoord[0]));
return geometry.covers(point);
} catch (IllegalArgumentException e) {
log.error("Invalid WKT format: {}", wkt, e);
throw e;
}
}
/**
* 判断指定坐标是否在WKT多边形内部(不进行坐标系转换)
* 注意:输入的坐标和WKT必须是同一个坐标系
*
* @param lat 纬度
* @param lon 经度
* @param wkt WKT格式的多边形字符串
* @return true表示点在多边形内,false表示点在多边形外
* @throws IllegalArgumentException 当WKT格式无效时抛出
*/
public static boolean isPointInPolygonWithoutTransform(double lat, double lon, String wkt) {
try {
// 直接解析WKT字符串为几何对象,不进行坐标系转换
WKTReader reader = WKT_READER_THREAD_LOCAL.get();
Geometry geometry = reader.read(wkt);
// 创建点对象,注意JTS中坐标顺序是(经度, 纬度)
Point point = GEOMETRY_FACTORY.createPoint(new Coordinate(lon, lat));
// 判断点是否在多边形内
return geometry.covers(point);
} catch (ParseException e) {
log.error("Invalid WKT format: {}", wkt, e);
throw new IllegalArgumentException("Invalid WKT format: " + wkt, e);
}
}
/**
* 判断指定WGS84坐标系的坐标是否在WGS84坐标系的WKT多边形内部
*
* @param lat 纬度(WGS84坐标系)
* @param lon 经度(WGS84坐标系)
* @param wkt WKT格式的多边形字符串(WGS84坐标系)
* @return true表示点在多边形内,false表示点在多边形外
* @throws IllegalArgumentException 当WKT格式无效时抛出
*/
public static boolean isWgs84PointInWgs84Polygon(double lat, double lon, String wkt) {
return isPointInPolygonWithoutTransform(lat, lon, wkt);
}
/**
* 判断指定高德坐标系(GCJ-02)的坐标是否在高德坐标系的WKT多边形内部
*
* @param lat 纬度(高德坐标系)
* @param lon 经度(高德坐标系)
* @param wkt WKT格式的多边形字符串(高德坐标系)
* @return true表示点在多边形内,false表示点在多边形外
* @throws IllegalArgumentException 当WKT格式无效时抛出
*/
public static boolean isGcj02PointInGcj02Polygon(double lat, double lon, String wkt) {
return isPointInPolygonWithoutTransform(lat, lon, wkt);
}
/**
* 计算指定坐标的GeoHash值
* 注意:输入的坐标为高德坐标系(GCJ-02),内部会自动转换为WGS84坐标系进行计算
*
* @param lat 纬度(高德坐标系)
* @param lon 经度(高德坐标系)
* @param precision GeoHash精度,取值范围1-12
* @return GeoHash字符串
*/
public static String calculateGeoHash(double lat, double lon, int precision) {
// 将高德坐标系转换为WGS84坐标系
double[] wgsCoord = CoordTransUtils.gcj02ToWgs84(lat, lon);
return GeoHash.withCharacterPrecision(wgsCoord[0], wgsCoord[1], precision).toBase32();
}
/**
* 将GeoHash集合转换为MultiPolygon,保持每个GeoHash的独立多边形形式
*
* @param geoHashes GeoHash字符串集合
* @return MultiPolygon 包含所有GeoHash对应的独立多边形,不进行合并
*/
public static MultiPolygon getPolygonsFromHashes(Set<String> geoHashes) {
if (geoHashes == null || geoHashes.isEmpty()) {
return GEOMETRY_FACTORY.createMultiPolygon(null);
}
Polygon[] polygons = new Polygon[geoHashes.size()];
int index = 0;
for (String hash : geoHashes) {
BoundingBox box = GeoHash.fromGeohashString(hash).getBoundingBox();
polygons[index++] = createGeoHashPolygon(box);
}
return GEOMETRY_FACTORY.createMultiPolygon(polygons);
}
/**
* 将GeoHash列表转换为MultiPolygon对象
*
* @param geoHashes GeoHash字符串集合
* @return 由GeoHash区域组成的MultiPolygon对象
*/
public static MultiPolygon convertHashesToMultiPolygon(Set<String> geoHashes) {
List<Polygon> polygons = new ArrayList<>(geoHashes.size());
for (String geoHash : geoHashes) {
BoundingBox box = GeoHash.fromGeohashString(geoHash).getBoundingBox();
polygons.add(createGeoHashPolygon(box));
}
// 合并所有多边形
Geometry union = null;
for (Polygon polygon : polygons) {
if (union == null) {
union = polygon;
continue;
}
union = union.union(polygon);
}
// 处理合并结果
if (union instanceof MultiPolygon) {
return (MultiPolygon) union;
}
if (union instanceof Polygon) {
return GEOMETRY_FACTORY.createMultiPolygon(new Polygon[]{(Polygon) union});
}
return GEOMETRY_FACTORY.createMultiPolygon(null);
}
private static Set<String> getIntersectingGeoHashesForGeometry(Geometry geometry, int precision) {
Set<String> geoHashes = new HashSet<>();
if (geometry instanceof MultiPolygon) {
MultiPolygon multiPolygon = (MultiPolygon) geometry;
for (int i = 0; i < multiPolygon.getNumGeometries(); i++) {
Polygon polygon = (Polygon) multiPolygon.getGeometryN(i);
geoHashes.addAll(calculateGeoHashesForPolygon(polygon, precision));
}
} else if (geometry instanceof Polygon) {
geoHashes.addAll(calculateGeoHashesForPolygon((Polygon) geometry, precision));
}
return geoHashes;
}
/**
* 计算多边形覆盖的GeoHash列表
*
* @param polygon 目标多边形
* @param precision GeoHash精度
* @return 与多边形相交的GeoHash集合
*/
private static Set<String> calculateGeoHashesForPolygon(Polygon polygon, int precision) {
Set<String> geoHashes = new LinkedHashSet<>();
// 获取多边形的边界框
Envelope envelope = polygon.getEnvelopeInternal();
// 创建完整覆盖边界框的最小GeoHash集合
Set<String> coveringHashes = getCoveringGeoHashes(
envelope.getMinY(), envelope.getMinX(),
envelope.getMaxY(), envelope.getMaxX(),
precision);
// 验证每个GeoHash是否与多边形相交
for (String hash : coveringHashes) {
if (intersectsWithPolygon(hash, polygon)) {
geoHashes.add(hash);
}
}
return geoHashes;
}
/**
* 判断GeoHash区域是否与多边形相交
*
* @param geoHash GeoHash字符串
* @param polygon 目标多边形
* @return 是否相交
*/
private static boolean intersectsWithPolygon(String geoHash, Polygon polygon) {
BoundingBox box = GeoHash.fromGeohashString(geoHash).getBoundingBox();
Polygon geoHashPoly = createGeoHashPolygon(box);
return polygon.intersects(geoHashPoly);
}
/**
* 创建表示GeoHash区域的多边形
*
* @param box GeoHash的边界框
* @return 表示GeoHash区域的多边形
*/
private static Polygon createGeoHashPolygon(BoundingBox box) {
Coordinate[] coords = new Coordinate[5];
coords[0] = new Coordinate(box.getWestLongitude(), box.getSouthLatitude());
coords[1] = new Coordinate(box.getEastLongitude(), box.getSouthLatitude());
coords[2] = new Coordinate(box.getEastLongitude(), box.getNorthLatitude());
coords[3] = new Coordinate(box.getWestLongitude(), box.getNorthLatitude());
// 闭合多边形
coords[4] = coords[0];
return GEOMETRY_FACTORY.createPolygon(coords);
}
private static void validatePrecision(int precision) {
if (precision < 1 || precision > 12) {
throw new IllegalArgumentException("Precision must be between 1 and 12");
}
}
/**
* 获取覆盖指定边界框的GeoHash集合
* 添加安全措施防止在极端纬度区域出现无限循环
*
* @param minLat 最小纬度
* @param minLon 最小经度
* @param maxLat 最大纬度
* @param maxLon 最大经度
* @param precision GeoHash精度
* @return 覆盖边界框的GeoHash集合
* @throws IllegalArgumentException 当坐标在极值区域或跨越国际日期变更线时抛出
*/
private static Set<String> getCoveringGeoHashes(double minLat, double minLon,
double maxLat, double maxLon,
int precision) {
minLat = Math.max(minLat, -90.0);
maxLat = Math.min(maxLat, 90.0);
minLon = normalizeLongitude(minLon);
maxLon = normalizeLongitude(maxLon);
// 处理极端纬度区域(接近南北极)- 直接抛出异常
if (minLat > MAX_EXTREME_LATITUDE || maxLat < MIN_EXTREME_LATITUDE) {
throw new IllegalArgumentException(String.format("坐标超出可计算GeoHash的纬度范围(%f, %f), 请使用纬度范围在%f到%f之间的坐标",
minLat, maxLat, MIN_EXTREME_LATITUDE, MAX_EXTREME_LATITUDE));
}
// 处理跨180度经线的特殊情况 - 在中国区域不会出现,直接抛出异常
if (minLon > maxLon) {
throw new IllegalArgumentException(String.format("经度范围异常: 最小经度(%f)大于最大经度(%f), 这在中国区域是不可能的情况,请检查输入坐标",
minLon, maxLon));
}
return getCoveringGeoHashesNoCrossing(minLat, minLon, maxLat, maxLon, precision);
}
/**
* 处理不跨越日期线的普通区域的GeoHash覆盖
*
* @param minLat 最小纬度
* @param minLon 最小经度
* @param maxLat 最大纬度
* @param maxLon 最大经度
* @param precision GeoHash精度
* @return 覆盖边界框的GeoHash集合
*/
private static Set<String> getCoveringGeoHashesNoCrossing(double minLat, double minLon,
double maxLat, double maxLon,
int precision) {
// 参数校验和规范化
minLat = Math.max(minLat, -90.0);
maxLat = Math.min(maxLat, 90.0);
minLon = normalizeLongitude(minLon);
maxLon = normalizeLongitude(maxLon);
// 极端纬度检查(应该已在上层方法中过滤,这里作为额外保护)
if (minLat > MAX_EXTREME_LATITUDE || maxLat < MIN_EXTREME_LATITUDE) {
throw new IllegalArgumentException(
String.format("坐标超出可计算GeoHash的纬度范围(%f, %f)", minLat, maxLat));
}
// 使用LinkedHashSet保证遍历顺序
Set<String> hashes = new LinkedHashSet<>();
// 计算边界的GeoHash
GeoHash bottomLeft = GeoHash.withCharacterPrecision(minLat, minLon, precision);
GeoHash topRight = GeoHash.withCharacterPrecision(maxLat, maxLon, precision);
// 从左下角开始添加所有可能的GeoHash
GeoHash current = bottomLeft;
// 行计数
int rowCount = 0;
// 按行遍历,从南向北
while (current != null &&
current.getBoundingBox().getNorthLatitude() <= topRight.getBoundingBox().getNorthLatitude() &&
rowCount < MAX_GEOHASH_ROWS) {
GeoHash rowHash = current;
// 列计数
int colCount = 0;
// 按列遍历,从西向东
while (rowHash != null &&
rowHash.getBoundingBox().getEastLongitude() <= topRight.getBoundingBox().getEastLongitude() &&
colCount < MAX_GEOHASH_COLS) {
hashes.add(rowHash.toBase32());
// 获取东侧相邻的GeoHash
rowHash = rowHash.getEasternNeighbour();
colCount++;
// 处理跨经线问题
if (rowHash != null && rowHash.getBoundingBox().getWestLongitude() >
rowHash.getBoundingBox().getEastLongitude()) {
break;
}
}
// 检查列是否达到上限,并立即记录警告日志
if (colCount >= MAX_GEOHASH_COLS) {
log.warn("GeoHash计算在第{}行达到列数限制: 边界({}, {}, {}, {}), 精度: {}, 当前行处理了{}个GeoHash",
rowCount + 1, minLat, minLon, maxLat, maxLon, precision, colCount);
}
// 获取北侧相邻的GeoHash
current = current.getNorthernNeighbour();
rowCount++;
}
// 如果达到行数限制,记录警告日志
if (rowCount >= MAX_GEOHASH_ROWS) {
log.warn("GeoHash计算达到行数限制: 边界({}, {}, {}, {}), 精度: {}, 获取到 {} 个GeoHash",
minLat, minLon, maxLat, maxLon, precision, hashes.size());
}
return hashes;
}
private static double normalizeLongitude(double lon) {
while (lon > 180.0) {
lon -= 360.0;
}
while (lon < -180.0) {
lon += 360.0;
}
return lon;
}
}

浙公网安备 33010602011771号