工具类
java
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.locationtech.jts.algorithm.ConvexHull;
import org.locationtech.jts.algorithm.MinimumBoundingCircle;
import org.locationtech.jts.algorithm.MinimumDiameter;
import org.locationtech.jts.algorithm.Orientation;
import org.locationtech.jts.algorithm.distance.DistanceToPoint;
import org.locationtech.jts.algorithm.distance.PointPairDistance;
import org.locationtech.jts.geom.*;
import org.locationtech.jts.geom.util.PolygonExtracter;
import org.locationtech.jts.io.WKTReader;
import org.locationtech.jts.io.WKTWriter;
import org.locationtech.jts.operation.buffer.BufferOp;
import org.locationtech.jts.operation.buffer.BufferParameters;
import org.locationtech.jts.operation.linemerge.LineMerger;
import org.locationtech.jts.operation.polygonize.Polygonizer;
import org.locationtech.jts.operation.union.UnaryUnionOp;
import org.locationtech.jts.operation.valid.IsValidOp;
import org.locationtech.jts.simplify.DouglasPeuckerSimplifier;
import org.locationtech.jts.simplify.TopologyPreservingSimplifier;
import org.locationtech.jts.simplify.VWSimplifier;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
/**
* 终极版:GeoJSON 转 WKT 工具类(适配达梦 ST_Geometry)
* 仅依赖 jts-core + jackson(项目自带,无需新增)
*/
public class GeoSpatialUtil {
private static final GeometryFactory geometryFactory = new GeometryFactory();
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final WKTWriter wktWriter = new WKTWriter();
/**
* 前端 GeoJSON 字符串 转 WKT(达梦数据库专用)
*/
public static String geoJsonToWkt(String geoJson) {
if (geoJson == null || geoJson.isEmpty()) {
return null;
}
try {
// 1. 解析 GeoJSON
JsonNode root = objectMapper.readTree(geoJson);
String type = root.get("type").asText();
JsonNode coordinatesNode = root.get("coordinates");
Geometry geometry;
// 2. 支持 LineString(你的路线数据就是这个)
if ("LineString".equalsIgnoreCase(type)) {
Coordinate[] coordinates = parseLineString(coordinatesNode);
geometry = geometryFactory.createLineString(coordinates);
} else {
throw new RuntimeException("不支持的空间类型:" + type);
}
// 3. 转 WKT
return wktWriter.write(geometry);
} catch (Exception e) {
throw new RuntimeException("GeoJSON 转换 WKT 失败:" + e.getMessage(), e);
}
}
/**
* 解析坐标
*/
private static Coordinate[] parseLineString(JsonNode coordinatesNode) {
Coordinate[] coordinates = new Coordinate[coordinatesNode.size()];
for (int i = 0; i < coordinatesNode.size(); i++) {
JsonNode point = coordinatesNode.get(i);
double x = point.get(0).asDouble();
double y = point.get(1).asDouble();
coordinates[i] = new Coordinate(x, y);
}
return coordinates;
}
/**
* WKT 字符串 转 前端 GeoJSON 字符串
* 支持 Point, LineString, Polygon 等常见类型
*/
public static String wktToGeoJson(String wkt) {
if (wkt == null || wkt.isEmpty()) {
return null;
}
try {
// 1. 使用 JTS 将 WKT 字符串解析为 Geometry 对象
Geometry geometry = new WKTReader().read(wkt);
// 2. 将 Geometry 对象转换为 GeoJSON 格式的 JsonNode
ObjectNode geoJsonNode = geometryToGeoJson(geometry);
// 3. 将 JsonNode 序列化为字符串
return objectMapper.writeValueAsString(geoJsonNode);
} catch (Exception e) {
throw new RuntimeException("WKT 转换 GeoJSON 失败:" + e.getMessage(), e);
}
}
/**
* 将 JTS Geometry 对象转换为 GeoJSON 的 JsonNode
*/
private static ObjectNode geometryToGeoJson(Geometry geometry) {
ObjectNode root = objectMapper.createObjectNode();
root.put("type", geometry.getGeometryType());
// 处理坐标系 (可选,但推荐)
// 如果 geometry 有 SRID,可以添加到 crs 属性中
if (geometry.getSRID() != 0) {
ObjectNode crs = objectMapper.createObjectNode();
crs.put("type", "name");
ObjectNode props = objectMapper.createObjectNode();
props.put("name", "EPSG:" + geometry.getSRID());
crs.set("properties", props);
root.set("crs", crs);
}
// 根据几何类型处理 coordinates
if (geometry instanceof Point) {
root.set("coordinates", coordinateToJsonNode(((Point) geometry).getCoordinate()));
} else if (geometry instanceof LineString) {
root.set("coordinates", lineStringToJsonNode((LineString) geometry));
} else if (geometry instanceof Polygon) {
root.set("coordinates", polygonToJsonNode((Polygon) geometry));
} else if (geometry instanceof MultiPoint) {
root.set("coordinates", multiPointToJsonNode((MultiPoint) geometry));
} else if (geometry instanceof MultiLineString) {
root.set("coordinates", multiLineStringToJsonNode((MultiLineString) geometry));
} else if (geometry instanceof MultiPolygon) {
root.set("coordinates", multiPolygonToJsonNode((MultiPolygon) geometry));
} else if (geometry instanceof GeometryCollection) {
// 处理几何集合,这里简化处理,实际可能需要递归
ArrayNode geometries = objectMapper.createArrayNode();
for (int i = 0; i < geometry.getNumGeometries(); i++) {
geometries.add(geometryToGeoJson(geometry.getGeometryN(i)));
}
root.set("geometries", geometries);
}
return root;
}
// --- 以下为各种几何类型转 JsonNode 的辅助方法 ---
private static ArrayNode coordinateToJsonNode(Coordinate coord) {
ArrayNode array = objectMapper.createArrayNode();
array.add(coord.x);
array.add(coord.y);
// 如果有 Z 坐标,也可以添加
if (!Double.isNaN(coord.getZ())) {
array.add(coord.getZ());
}
return array;
}
private static ArrayNode lineStringToJsonNode(LineString lineString) {
ArrayNode array = objectMapper.createArrayNode();
for (Coordinate coord : lineString.getCoordinates()) {
array.add(coordinateToJsonNode(coord));
}
return array;
}
private static ArrayNode polygonToJsonNode(Polygon polygon) {
ArrayNode array = objectMapper.createArrayNode();
// 添加外环
array.add(lineStringToJsonNode(polygon.getExteriorRing()));
// 添加内环(洞)
for (int i = 0; i < polygon.getNumInteriorRing(); i++) {
array.add(lineStringToJsonNode(polygon.getInteriorRingN(i)));
}
return array;
}
// 为完整性提供多点、多线、多面的转换方法
private static ArrayNode multiPointToJsonNode(MultiPoint multiPoint) {
ArrayNode array = objectMapper.createArrayNode();
for (int i = 0; i < multiPoint.getNumGeometries(); i++) {
array.add(coordinateToJsonNode(((Point) multiPoint.getGeometryN(i)).getCoordinate()));
}
return array;
}
private static ArrayNode multiLineStringToJsonNode(MultiLineString multiLineString) {
ArrayNode array = objectMapper.createArrayNode();
for (int i = 0; i < multiLineString.getNumGeometries(); i++) {
array.add(lineStringToJsonNode((LineString) multiLineString.getGeometryN(i)));
}
return array;
}
private static ArrayNode multiPolygonToJsonNode(MultiPolygon multiPolygon) {
ArrayNode array = objectMapper.createArrayNode();
for (int i = 0; i < multiPolygon.getNumGeometries(); i++) {
array.add(polygonToJsonNode((Polygon) multiPolygon.getGeometryN(i)));
}
return array;
}
// ========================================================================
// 以下为新增扩展模块(保持上述原有方法完全不变)
// ========================================================================
// ==================== 常量与内部类型定义 ====================
/** 地球平均半径(单位:米) */
private static final double EARTH_RADIUS_METERS = 6371000.0;
/** 圆周率 / 180,用于角度转弧度 */
private static final double DEG_TO_RAD = Math.PI / 180.0;
/** 180 / 圆周率,用于弧度转角度 */
private static final double RAD_TO_DEG = 180.0 / Math.PI;
/** GCJ02 偏移计算用常量:长半轴(单位:米) */
private static final double GCJ_A = 6378245.0;
/** GCJ02 偏移计算用常量:第一偏心率平方 */
private static final double GCJ_EE = 0.00669342162296594323;
/**
* 坐标系统枚举
* <ul>
* <li>WGS84: GPS 设备原始坐标,国际标准</li>
* <li>GCJ02: 国测局坐标系(火星坐标系),高德/腾讯使用</li>
* <li>CGCS2000: 2000 国家大地坐标系,国内官方标准,与 WGS84 厘米级差异</li>
* </ul>
*/
public enum CoordSystem {
WGS84, GCJ02, CGCS2000
}
/**
* 经纬度坐标点(不可变对象)
*/
public static class LatLng {
public final double lng;
public final double lat;
public LatLng(double lng, double lat) {
this.lng = lng;
this.lat = lat;
}
/** 转为 JTS Coordinate(x=lng, y=lat) */
public Coordinate toCoordinate() {
return new Coordinate(lng, lat);
}
/** 从 JTS Coordinate 创建 */
public static LatLng fromCoordinate(Coordinate coord) {
return new LatLng(coord.x, coord.y);
}
@Override
public String toString() {
return "(" + lng + ", " + lat + ")";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof LatLng)) return false;
LatLng latLng = (LatLng) o;
return Double.compare(latLng.lng, lng) == 0 && Double.compare(latLng.lat, lat) == 0;
}
@Override
public int hashCode() {
return Objects.hash(lng, lat);
}
}
// ==================== 模块一:坐标系统转换 ====================
/**
* 坐标系统转换(通用入口)
* <p>
* 支持 WGS84 ↔ GCJ02 双向转换,WGS84 与 CGCS2000 之间精度极高(厘米级),直接等同返回。
* </p>
*
* @param lng 经度
* @param lat 纬度
* @param from 原始坐标系
* @param to 目标坐标系
* @return 转换后的坐标点
* @throws IllegalArgumentException 不支持的转换组合时抛出
*/
public static LatLng convertCoord(double lng, double lat, CoordSystem from, CoordSystem to) {
if (from == to) {
return new LatLng(lng, lat);
}
// CGCS2000 与 WGS84 厘米级差异,直接等同处理
if (from == CoordSystem.CGCS2000) from = CoordSystem.WGS84;
if (to == CoordSystem.CGCS2000) to = CoordSystem.WGS84;
if (from == to) {
return new LatLng(lng, lat);
}
if (from == CoordSystem.WGS84 && to == CoordSystem.GCJ02) {
return wgs84ToGcj02(lng, lat);
}
if (from == CoordSystem.GCJ02 && to == CoordSystem.WGS84) {
return gcj02ToWgs84(lng, lat);
}
throw new IllegalArgumentException("不支持的坐标转换: " + from + " -> " + to);
}
/**
* WGS84 → GCJ02(火星坐标系)
* <p>
* 将 GPS 设备原始坐标(WGS84)转为国测局加密坐标(GCJ02),
* 适用于高德地图、腾讯地图等国内地图服务。
* </p>
* <p>
* 核心算法:根据经纬度计算非线性偏移量,叠加到原始坐标上。
* 中国境外坐标不做偏移处理。
* </p>
*
* @param lng WGS84 经度
* @param lat WGS84 纬度
* @return GCJ02 坐标
*/
public static LatLng wgs84ToGcj02(double lng, double lat) {
if (!isInChina(lng, lat)) {
return new LatLng(lng, lat);
}
double[] d = delta(lng, lat);
return new LatLng(lng + d[0], lat + d[1]);
}
/**
* GCJ02 → WGS84(火星坐标系转 GPS 原始坐标)
* <p>
* 采用迭代逼近法(5 次迭代)将 GCJ02 反向转换为 WGS84,
* 精度可控制在 0.1 米以内。
* </p>
*
* @param lng GCJ02 经度
* @param lat GCJ02 纬度
* @return WGS84 坐标
*/
public static LatLng gcj02ToWgs84(double lng, double lat) {
if (!isInChina(lng, lat)) {
return new LatLng(lng, lat);
}
double wgsLng = lng, wgsLat = lat;
for (int i = 0; i < 5; i++) {
double[] d = delta(wgsLng, wgsLat);
wgsLng = lng - d[0];
wgsLat = lat - d[1];
}
return new LatLng(wgsLng, wgsLat);
}
/**
* 批量转换坐标数组
*
* @param coords 坐标数组,每个元素为 [经度, 纬度]
* @param from 原始坐标系
* @param to 目标坐标系
* @return 转换后的坐标数组
*/
public static double[][] convertCoords(double[][] coords, CoordSystem from, CoordSystem to) {
double[][] result = new double[coords.length][2];
for (int i = 0; i < coords.length; i++) {
LatLng p = convertCoord(coords[i][0], coords[i][1], from, to);
result[i][0] = p.lng;
result[i][1] = p.lat;
}
return result;
}
/**
* 判断经纬度是否在中国范围内
* <p>
* 中国大致范围:东经 73°~135°,北纬 3°~54°。
* 此范围用于决定是否需要 GCJ02 偏移。
* </p>
*
* @param lng 经度
* @param lat 纬度
* @return 在中国范围内返回 true
*/
public static boolean isInChina(double lng, double lat) {
return lng > 73.0 && lng < 135.0 && lat > 3.0 && lat < 54.0;
}
/**
* 计算 GCJ02 偏移量(核心算法)
* <p>
* 根据 WGS84 坐标计算非线性偏移量 delta(lng, lat),
* 该偏移量是基于中国国家测绘局发布的加密算法推导得出。
* </p>
*
* @param lng 经度(WGS84)
* @param lat 纬度(WGS84)
* @return double[2] = {经度偏移量, 纬度偏移量}(单位:度)
*/
private static double[] delta(double lng, double lat) {
double radLat = lat * DEG_TO_RAD;
double magic = Math.sin(radLat);
magic = 1 - GCJ_EE * magic * magic;
double sqrtMagic = Math.sqrt(magic);
double dLng = (lng - 105.0) * DEG_TO_RAD;
double dLat = (lat - 35.0) * DEG_TO_RAD;
// 纬度偏移计算(由多条三角函数曲线拟合)
double dLatCalc = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat
+ 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng));
dLatCalc += (20.0 * Math.sin(6.0 * lng * Math.PI) + 20.0 * Math.sin(2.0 * lng * Math.PI)) * 2.0 / 3.0;
dLatCalc += (20.0 * Math.sin(lat * Math.PI) + 40.0 * Math.sin(lat / 3.0 * Math.PI)) * 2.0 / 3.0;
dLatCalc += (160.0 * Math.sin(lat / 12.0 * Math.PI) + 320.0 * Math.sin(lat * Math.PI / 30.0)) * 2.0 / 3.0;
// 经度偏移计算
double dLngCalc = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng));
dLngCalc += (20.0 * Math.sin(6.0 * lng * Math.PI) + 20.0 * Math.sin(2.0 * lng * Math.PI)) * 2.0 / 3.0;
dLngCalc += (20.0 * Math.sin(lng * Math.PI) + 40.0 * Math.sin(lng / 3.0 * Math.PI)) * 2.0 / 3.0;
dLngCalc += (150.0 * Math.sin(lng / 12.0 * Math.PI) + 300.0 * Math.sin(lng / 30.0 * Math.PI)) * 2.0 / 3.0;
// 将偏移量(米)转换为度
double radLatA = (GCJ_A * (1 - GCJ_EE)) / (magic * sqrtMagic) * DEG_TO_RAD;
double radLngA = (GCJ_A * Math.cos(radLat)) / sqrtMagic * DEG_TO_RAD;
return new double[]{dLngCalc / radLngA, dLatCalc / radLatA};
}
// ==================== 模块二:球面距离与度量计算 ====================
/**
* 球面距离计算(Haversine 公式)
* <p>
* 计算两个经纬度之间的地球表面最短距离(大圆距离),
* 基于球面三角学中的半正矢公式,精度约 0.5%(相对于椭球模型)。
* </p>
* <p>
* 算法:a = sin²(Δlat/2) + cos(lat1)·cos(lat2)·sin²(Δlng/2)<br>
* c = 2·atan2(√a, √(1-a))<br>
* d = R·c (R = 地球平均半径 6371km)
* </p>
*
* @param lng1 点1 经度
* @param lat1 点1 纬度
* @param lng2 点2 经度
* @param lat2 点2 纬度
* @return 球面距离(单位:米)
*/
public static double haversineDistance(double lng1, double lat1, double lng2, double lat2) {
double radLat1 = lat1 * DEG_TO_RAD;
double radLat2 = lat2 * DEG_TO_RAD;
double dLng = (lng2 - lng1) * DEG_TO_RAD;
double dLat = radLat2 - radLat1;
double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
+ Math.cos(radLat1) * Math.cos(radLat2) * Math.sin(dLng / 2) * Math.sin(dLng / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return EARTH_RADIUS_METERS * c;
}
/**
* Haversine 距离(LngLat 对象版)
*
* @param p1 点1
* @param p2 点2
* @return 距离(单位:米)
*/
public static double haversineDistance(LatLng p1, LatLng p2) {
return haversineDistance(p1.lng, p1.lat, p2.lng, p2.lat);
}
/**
* 计算多点之间的累计折线长度
* <p>
* 依次累加点0→点1→点2→... 每个相邻点对的球面距离。
* </p>
*
* @param points 经纬度点列表,至少需要 2 个点
* @return 累计折线长度(单位:米),不足 2 个点返回 0
*/
public static double totalDistance(List<LatLng> points) {
if (points == null || points.size() < 2) {
return 0;
}
double total = 0;
for (int i = 1; i < points.size(); i++) {
total += haversineDistance(points.get(i - 1), points.get(i));
}
return total;
}
/**
* 计算 JTS LineString 的球面长度
* <p>
* 将平面 LineString 的每个线段用 Haversine 公式重新计算球面距离,
* 适用于经纬度坐标的线串。
* </p>
*
* @param lineString JTS LineString(要求坐标为经纬度)
* @return 球面长度(单位:米)
*/
public static double lineStringLength(LineString lineString) {
Coordinate[] coords = lineString.getCoordinates();
double total = 0;
for (int i = 1; i < coords.length; i++) {
total += haversineDistance(coords[i - 1].x, coords[i - 1].y, coords[i].x, coords[i].y);
}
return total;
}
/**
* 计算两点之间的方位角(正北方向,顺时针)
* <p>
* 方位角是指从起点指向终点的方向与正北方向的夹角,
* 顺时针递增,范围 0°~360°。
* </p>
* <p>
* 算法:θ = atan2(sin(Δlng)·cos(lat2), cos(lat1)·sin(lat2) - sin(lat1)·cos(lat2)·cos(Δlng))
* </p>
*
* @param lng1 起点经度
* @param lat1 起点纬度
* @param lng2 终点经度
* @param lat2 终点纬度
* @return 方位角(单位:度),0°=正北,90°=正东,180°=正南,270°=正西
*/
public static double bearing(double lng1, double lat1, double lng2, double lat2) {
double radLat1 = lat1 * DEG_TO_RAD;
double radLat2 = lat2 * DEG_TO_RAD;
double dLng = (lng2 - lng1) * DEG_TO_RAD;
double y = Math.sin(dLng) * Math.cos(radLat2);
double x = Math.cos(radLat1) * Math.sin(radLat2)
- Math.sin(radLat1) * Math.cos(radLat2) * Math.cos(dLng);
double bearing = Math.atan2(y, x) * RAD_TO_DEG;
return (bearing + 360) % 360;
}
/**
* 在球面上根据起点、方位角和距离计算终点坐标(航位推算法)
* <p>
* 算法:给定起点 (lat1, lng1)、方位角 θ 和距离 d,计算终点的经纬度。
* 也称为"直接问题"(Direct Geodetic Problem)的球面近似解法。
* </p>
*
* @param lng 起点经度
* @param lat 起点纬度
* @param bearing 方位角(单位:度),0=正北,顺时针
* @param distanceMeters 从起点沿方位角方向前进的距离(单位:米)
* @return 终点坐标
*/
public static LatLng destinationPoint(double lng, double lat, double bearing, double distanceMeters) {
double radLat = lat * DEG_TO_RAD;
double radLng = lng * DEG_TO_RAD;
double radBearing = bearing * DEG_TO_RAD;
double angularDistance = distanceMeters / EARTH_RADIUS_METERS;
double destLat = Math.asin(Math.sin(radLat) * Math.cos(angularDistance)
+ Math.cos(radLat) * Math.sin(angularDistance) * Math.cos(radBearing));
double destLng = radLng + Math.atan2(
Math.sin(radBearing) * Math.sin(angularDistance) * Math.cos(radLat),
Math.cos(angularDistance) - Math.sin(radLat) * Math.sin(destLat));
return new LatLng(destLng * RAD_TO_DEG, destLat * RAD_TO_DEG);
}
/**
* 计算两个经纬度点的中点坐标
* <p>
* 使用球面线性插值计算大圆路径上的中点。
* </p>
*
* @param lng1 点1 经度
* @param lat1 点1 纬度
* @param lng2 点2 经度
* @param lat2 点2 纬度
* @return 中点坐标
*/
public static LatLng midpoint(double lng1, double lat1, double lng2, double lat2) {
double radLat1 = lat1 * DEG_TO_RAD;
double radLng1 = lng1 * DEG_TO_RAD;
double radLat2 = lat2 * DEG_TO_RAD;
double dLng = (lng2 - lng1) * DEG_TO_RAD;
double bx = Math.cos(radLat2) * Math.cos(dLng);
double by = Math.cos(radLat2) * Math.sin(dLng);
double midLat = Math.atan2(
Math.sin(radLat1) + Math.sin(radLat2),
Math.sqrt((Math.cos(radLat1) + bx) * (Math.cos(radLat1) + bx) + by * by));
double midLng = radLng1 + Math.atan2(by, Math.cos(radLat1) + bx);
return new LatLng(midLng * RAD_TO_DEG, midLat * RAD_TO_DEG);
}
// ==================== 模块三:点到线的空间分析 ====================
/**
* 点到线段(或折线)的最近距离与投影信息
* <p>
* 包含点到几何对象的最短距离、垂足(最近点)坐标、投影比例。
* </p>
*/
public static class PointToLineResult {
/** 点到线段的最短球面距离(单位:米) */
public final double distance;
/** 垂足(或最近点)的经纬度坐标 */
public final LatLng projectionPoint;
/** 投影点在线段上的比例 0~1,0 表示起点,1 表示终点 */
public final double ratio;
public PointToLineResult(double distance, LatLng projectionPoint, double ratio) {
this.distance = distance;
this.projectionPoint = projectionPoint;
this.ratio = ratio;
}
}
/**
* 计算点到线段的最近距离和垂足坐标(向量投影法)
* <p>
* 算法原理:<br>
* 1. 计算向量 AB 和 AP<br>
* 2. 计算投影比例 t = (AP·AB) / (AB·AB)<br>
* 3. 将 t 限制到 [0, 1] 范围内(保证垂足在线段上)<br>
* 4. 计算垂足坐标 = A + t * AB<br>
* 5. 用 Haversine 公式计算点到垂足的球面距离
* </p>
* <p>
* 适用于线段较短(< 10km)的场景,平面近似误差可忽略。
* </p>
*
* @param px 点 P 经度
* @param py 点 P 纬度
* @param ax 线段起点 A 经度
* @param ay 线段起点 A 纬度
* @param bx 线段终点 B 经度
* @param by 线段终点 B 纬度
* @return 点线分析结果,包含距离(米)、垂足坐标、投影比例
*/
public static PointToLineResult pointToSegment(
double px, double py, double ax, double ay, double bx, double by) {
double abx = bx - ax;
double aby = by - ay;
double apx = px - ax;
double apy = py - ay;
double ab2 = abx * abx + aby * aby;
if (ab2 == 0) {
// 线段退化为一个点
double dist = haversineDistance(px, py, ax, ay);
return new PointToLineResult(dist, new LatLng(ax, ay), 0);
}
double t = (apx * abx + apy * aby) / ab2;
double clampedT = Math.max(0, Math.min(1, t));
double projX = ax + clampedT * abx;
double projY = ay + clampedT * aby;
double distance = haversineDistance(px, py, projX, projY);
return new PointToLineResult(distance, new LatLng(projX, projY), clampedT);
}
/**
* 计算点到折线(LineString)的全局最近距离和投影点
* <p>
* 遍历折线的每个线段,用 pointToSegment 找出全局最近点,
* 同时计算投影点在整个折线上的全局比例(基于球面距离加权)。
* </p>
*
* @param point 待查询点
* @param lineString 折线对象
* @return 点到折线的分析结果
*/
public static PointToLineResult pointToLineString(LatLng point, LineString lineString) {
Coordinate[] coords = lineString.getCoordinates();
if (coords.length < 2) {
double dist = haversineDistance(point.lng, point.lat, coords[0].x, coords[0].y);
return new PointToLineResult(dist, new LatLng(coords[0].x, coords[0].y), 0);
}
PointToLineResult best = null;
double minDist = Double.MAX_VALUE;
double accumulatedLength = 0;
// 计算每段长度(用于全局比例加权)
double[] segmentLengths = new double[coords.length - 1];
double totalLength = 0;
for (int i = 0; i < coords.length - 1; i++) {
double segLen = haversineDistance(
coords[i].x, coords[i].y, coords[i + 1].x, coords[i + 1].y);
segmentLengths[i] = segLen;
totalLength += segLen;
}
for (int i = 0; i < coords.length - 1; i++) {
PointToLineResult result = pointToSegment(
point.lng, point.lat,
coords[i].x, coords[i].y,
coords[i + 1].x, coords[i + 1].y);
if (result.distance < minDist) {
minDist = result.distance;
double globalRatio = (totalLength > 0)
? (accumulatedLength + result.ratio * segmentLengths[i]) / totalLength
: 0;
best = new PointToLineResult(result.distance, result.projectionPoint, globalRatio);
}
accumulatedLength += segmentLengths[i];
}
return best;
}
/**
* 使用 JTS DistanceToPoint 精确计算点到折线的最短距离
* <p>
* 由 JTS 内置算法计算,比平面向量法更精确,支持任意几何类型。
* </p>
*
* @param point 待查询点(JTS Point)
* @param geometry 目标几何对象(任意类型)
* @return 最短距离(单位:度的平面距离,需根据场景自行转换)
*/
public static double distanceToGeometry(Point point, Geometry geometry) {
PointPairDistance ppd = new PointPairDistance();
DistanceToPoint.computeDistance(geometry, point.getCoordinate(), ppd);
return ppd.getDistance();
}
/**
* 从一组点中找出距离参考点最近的点
*
* @param points 待搜索的点列表
* @param refLng 参考点经度
* @param refLat 参考点纬度
* @return 最近的点(列表为空返回 null)
*/
public static LatLng nearestPoint(List<LatLng> points, double refLng, double refLat) {
if (points == null || points.isEmpty()) {
return null;
}
LatLng nearest = null;
double minDist = Double.MAX_VALUE;
for (LatLng p : points) {
double dist = haversineDistance(refLng, refLat, p.lng, p.lat);
if (dist < minDist) {
minDist = dist;
nearest = p;
}
}
return nearest;
}
// ==================== 模块四:沿线插值与桩号计算 ====================
/**
* 在折线上按照距离进行线性插值
* <p>
* 给定从起点算起的距离(单位:米),返回折线上对应位置的经纬度坐标。
* 如果给定的距离超过折线总长,返回终点坐标。
* </p>
* <p>
* 算法:逐个线段累加球面距离,找到目标距离所在的线段,
* 在该线段上按剩余距离的比例进行线性插值。
* </p>
*
* @param lineString 折线(LineString,坐标应为经纬度)
* @param distanceMeters 从起点算起的距离(单位:米)
* @return 插值点的经纬度坐标
*/
public static LatLng interpolateAlongLine(LineString lineString, double distanceMeters) {
Coordinate[] coords = lineString.getCoordinates();
if (coords.length < 2) {
return new LatLng(coords[0].x, coords[0].y);
}
double accumulated = 0;
for (int i = 0; i < coords.length - 1; i++) {
double segLen = haversineDistance(
coords[i].x, coords[i].y, coords[i + 1].x, coords[i + 1].y);
if (accumulated + segLen >= distanceMeters) {
double remain = distanceMeters - accumulated;
double ratio = (segLen > 0) ? remain / segLen : 0;
double lng = coords[i].x + (coords[i + 1].x - coords[i].x) * ratio;
double lat = coords[i].y + (coords[i + 1].y - coords[i].y) * ratio;
return new LatLng(lng, lat);
}
accumulated += segLen;
}
// 超出总长,返回终点
Coordinate end = coords[coords.length - 1];
return new LatLng(end.x, end.y);
}
/**
* 根据投影比例计算桩号数值
* <p>
* 桩号 = 起点桩号 + (终点桩号 - 起点桩号) × 投影比例
* </p>
*
* @param qdzh 起点桩号(单位:公里),例如 1000.0
* @param zdzh 终点桩号(单位:公里),例如 1050.0
* @param locateRatio 投影比例(0~1),0=起点,1=终点
* @return 插值后的桩号(单位:公里)
*/
public static double calcStakeValue(double qdzh, double zdzh, double locateRatio) {
return qdzh + (zdzh - qdzh) * locateRatio;
}
/**
* 将桩号数值格式化为桩号字符串
* <p>
* 例如:1025.300 → "K1025+300"<br>
* 公里部分取整数,小数部分转换为米(×1000)后四舍五入。
* </p>
*
* @param stakeValue 桩号数值(单位:公里)
* @return 格式化字符串,例如 "K1025+300"
*/
public static String formatStakeNo(double stakeValue) {
int km = (int) stakeValue;
double m = (stakeValue - km) * 1000;
int mInt = (int) Math.round(m);
if (mInt >= 1000) {
km++;
mInt = 0;
}
return "K" + km + "+" + String.format("%03d", mInt);
}
/**
* 将桩号字符串解析为数值
* <p>
* 例如:"K1025+300" → 1025.300<br>
* 支持 "K" 前缀可选,格式 "K公里+米" 或 "公里+米"
* </p>
*
* @param stakeNo 桩号字符串
* @return 桩号数值(单位:公里)
*/
public static double parseStakeNo(String stakeNo) {
if (StrUtil.isBlank(stakeNo)) {
return 0;
}
String clean = stakeNo.toUpperCase().replace("K", "").trim();
String[] parts = clean.split("\\+");
if (parts.length == 2) {
try {
int km = Integer.parseInt(parts[0].trim());
int m = Integer.parseInt(parts[1].trim());
return km + m / 1000.0;
} catch (NumberFormatException e) {
// 忽略,尝试直接解析
}
}
try {
return Double.parseDouble(clean);
} catch (NumberFormatException e) {
return 0;
}
}
/**
* 在折线上按比例插值
* <p>
* 与 interpolateAlongLine 类似,但使用比例(0~1)代替距离。
* 常用于已知 0~1 比例需要获取坐标的场景。
* </p>
*
* @param lineString 折线
* @param ratio 比例,0=起点,1=终点
* @return 插值点坐标
*/
public static LatLng interpolateByRatio(LineString lineString, double ratio) {
double totalLength = lineStringLength(lineString);
return interpolateAlongLine(lineString, totalLength * Math.max(0, Math.min(1, ratio)));
}
// ==================== 模块五:空间关系判断 ====================
/**
* 空间关系枚举
* <p>
* 用于标识两个几何对象之间的 DE-9IM 空间关系。
* 基于 JTS 的 Geometry 空间关系方法。
* </p>
*/
public enum SpatialRelation {
/** 相等(几何形状完全相同) */
EQUALS,
/** 相离(没有共同点) */
DISJOINT,
/** 相交(有共同点) */
INTERSECTS,
/** 接触(边界相交但内部不相交) */
TOUCHES,
/** 交叉(线/面相互穿过,维度下降) */
CROSSES,
/** 内部(完全在另一个几何内部) */
WITHIN,
/** 包含(完全包含另一个几何) */
CONTAINS,
/** 重叠(内部有重叠区域,维度相同) */
OVERLAPS,
/** 覆盖(与 contains 类似,但允许边界重合) */
COVERS,
/** 被覆盖(与 within 类似,但允许边界重合) */
COVERED_BY
}
/**
* 判断两个几何对象之间的空间关系
* <p>
* 底层调用 JTS 的 DE-9IM(Dimensionally Extended 9-Intersection Model)
* 空间关系判断矩阵。
* </p>
*
* @param wkt1 几何1 的 WKT 字符串
* @param wkt2 几何2 的 WKT 字符串
* @param relation 要判断的空间关系类型
* @return 满足指定关系返回 true
*/
public static boolean checkSpatialRelation(String wkt1, String wkt2, SpatialRelation relation) {
try {
Geometry g1 = new WKTReader().read(wkt1);
Geometry g2 = new WKTReader().read(wkt2);
return checkSpatialRelation(g1, g2, relation);
} catch (Exception e) {
throw new RuntimeException("空间关系判断失败:" + e.getMessage(), e);
}
}
/**
* 判断两个 Geometry 对象之间的空间关系
*
* @param g1 几何1
* @param g2 几何2
* @param relation 空间关系类型
* @return 满足返回 true
*/
public static boolean checkSpatialRelation(Geometry g1, Geometry g2, SpatialRelation relation) {
switch (relation) {
case EQUALS:
return g1.equals(g2);
case DISJOINT:
return g1.disjoint(g2);
case INTERSECTS:
return g1.intersects(g2);
case TOUCHES:
return g1.touches(g2);
case CROSSES:
return g1.crosses(g2);
case WITHIN:
return g1.within(g2);
case CONTAINS:
return g1.contains(g2);
case OVERLAPS:
return g1.overlaps(g2);
case COVERS:
return g1.covers(g2);
case COVERED_BY:
return g1.coveredBy(g2);
default:
throw new IllegalArgumentException("不支持的空间关系: " + relation);
}
}
/**
* 判断点是否在线的缓冲范围内(简化版路网邻近查询)
*
* @param pointLng 点经度
* @param pointLat 点纬度
* @param lineWkt 线的 WKT 字符串
* @param bufferMeters 缓冲区半径(单位:米)
* @return 点在缓冲区内返回 true
*/
public static boolean isPointNearLine(double pointLng, double pointLat, String lineWkt, double bufferMeters) {
try {
Geometry point = geometryFactory.createPoint(new Coordinate(pointLng, pointLat));
Geometry line = new WKTReader().read(lineWkt);
double bufferDegrees = bufferMeters / 111320.0;
Geometry buffer = line.buffer(bufferDegrees);
return buffer.intersects(point);
} catch (Exception e) {
throw new RuntimeException("点线邻近判断失败:" + e.getMessage(), e);
}
}
/**
* 判断一个点是否在多边形区域内
*
* @param lng 经度
* @param lat 纬度
* @param polygonWkt 多边形的 WKT 字符串
* @return 点在多边形内返回 true
*/
public static boolean isPointInPolygon(double lng, double lat, String polygonWkt) {
try {
Geometry point = geometryFactory.createPoint(new Coordinate(lng, lat));
Geometry polygon = new WKTReader().read(polygonWkt);
return polygon.contains(point);
} catch (Exception e) {
throw new RuntimeException("点在多边形判断失败:" + e.getMessage(), e);
}
}
/**
* 判断点是否在矩形边界框内
*
* @param lng 经度
* @param lat 纬度
* @param minLng 最小经度
* @param minLat 最小纬度
* @param maxLng 最大经度
* @param maxLat 最大纬度
* @return 点在矩形内返回 true
*/
public static boolean isInBoundingBox(double lng, double lat,
double minLng, double minLat,
double maxLng, double maxLat) {
return lng >= minLng && lng <= maxLng
&& lat >= minLat && lat <= maxLat;
}
/**
* 判断两个矩形边界框是否相交
*
* @param minLng1 框1 最小经度
* @param minLat1 框1 最小纬度
* @param maxLng1 框1 最大经度
* @param maxLat1 框1 最大纬度
* @param minLng2 框2 最小经度
* @param minLat2 框2 最小纬度
* @param maxLng2 框2 最大经度
* @param maxLat2 框2 最大纬度
* @return 相交返回 true
*/
public static boolean boundingBoxesIntersect(
double minLng1, double minLat1, double maxLng1, double maxLat1,
double minLng2, double minLat2, double maxLng2, double maxLat2) {
return !(maxLng1 < minLng2 || minLng1 > maxLng2
|| maxLat1 < minLat2 || minLat1 > maxLat2);
}
// ==================== 模块六:缓冲区分析 ====================
/**
* 对几何对象生成缓冲区
* <p>
* 缓冲区即几何对象周围指定距离的区域,常用于"周边分析"、"邻近查询"。
* 正数向外扩展,负数向内收缩。
* </p>
* <p>
* 底层使用 JTS BufferOp,缓冲区边界以度为单位。
* 粗略换算:1° ≈ 111320 米(赤道附近),中纬度地区需根据实际纬度微调。
* </p>
*
* @param wkt 几何对象的 WKT 字符串
* @param bufferMeters 缓冲区半径(单位:米),正数向外,负数向内
* @param quadrantSegments 每象限的线段数(控制边界平滑度,默认 8,越大越平滑)
* @return 缓冲区几何的 WKT 字符串
*/
public static String buffer(String wkt, double bufferMeters, int quadrantSegments) {
try {
Geometry geometry = new WKTReader().read(wkt);
double bufferDegrees = bufferMeters / 111320.0;
BufferParameters bp = new BufferParameters();
bp.setQuadrantSegments(quadrantSegments);
Geometry bufferGeom = BufferOp.bufferOp(geometry, bufferDegrees, bp);
return wktWriter.write(bufferGeom);
} catch (Exception e) {
throw new RuntimeException("缓冲区分析失败:" + e.getMessage(), e);
}
}
/**
* 缓冲区分析(使用默认平滑度)
*
* @param wkt WKT 字符串
* @param bufferMeters 缓冲区半径(米)
* @return 缓冲区 WKT
*/
public static String buffer(String wkt, double bufferMeters) {
return buffer(wkt, bufferMeters, BufferParameters.DEFAULT_QUADRANT_SEGMENTS);
}
/**
* 判断一个点是否在几何对象的缓冲区内
*
* @param pointLng 点经度
* @param pointLat 点纬度
* @param targetWkt 目标几何 WKT
* @param bufferMeters 缓冲区半径(米)
* @return 点在缓冲区内返回 true
*/
public static boolean isWithinBuffer(double pointLng, double pointLat,
String targetWkt, double bufferMeters) {
try {
Geometry point = geometryFactory.createPoint(new Coordinate(pointLng, pointLat));
Geometry target = new WKTReader().read(targetWkt);
double bufferDegrees = bufferMeters / 111320.0;
Geometry bufferGeom = target.buffer(bufferDegrees);
return bufferGeom.contains(point);
} catch (Exception e) {
throw new RuntimeException("缓冲区包含判断失败:" + e.getMessage(), e);
}
}
// ==================== 模块七:空间计算(面积、周长、质心、凸包等) ====================
/**
* 计算多边形的面积
* <p>
* 使用 JTS 的 getArea() 获取平面面积(度²),然后粗略换算为平方米。
* 中纬度地区的近似换算系数:1平方度 ≈ (111320)² 平方米。
* </p>
* <p>
* 注意:对于大范围区域,此近似换算误差较大,
* 精确计算应使用球面/椭球面面积公式。
* </p>
*
* @param polygonWkt 多边形的 WKT 字符串
* @return 近似面积(单位:平方米)
*/
public static double area(String polygonWkt) {
try {
Geometry geometry = new WKTReader().read(polygonWkt);
double areaDegSq = geometry.getArea();
// 粗略换算:1° ≈ 111320 米
return areaDegSq * 111320 * 111320;
} catch (Exception e) {
throw new RuntimeException("面积计算失败:" + e.getMessage(), e);
}
}
/**
* 计算多边形的周长
*
* @param polygonWkt 多边形的 WKT 字符串
* @return 周长(单位:米)
*/
public static double perimeter(String polygonWkt) {
try {
Geometry geometry = new WKTReader().read(polygonWkt);
double lengthDeg = geometry.getLength();
return lengthDeg * 111320;
} catch (Exception e) {
throw new RuntimeException("周长计算失败:" + e.getMessage(), e);
}
}
/**
* 计算几何对象的质心(几何中心)
* <p>
* 质心是几何对象形状的"重心"位置,对于简单多边形,
* 质心一定在形状内部。使用 JTS 的 getCentroid() 方法。
* </p>
*
* @param wkt 几何对象的 WKT 字符串
* @return 质心坐标
*/
public static LatLng centroid(String wkt) {
try {
Geometry geometry = new WKTReader().read(wkt);
Point center = geometry.getCentroid();
return new LatLng(center.getX(), center.getY());
} catch (Exception e) {
throw new RuntimeException("质心计算失败:" + e.getMessage(), e);
}
}
/**
* 计算几何对象的内部点(保证在几何内部)
* <p>
* 与质心的区别:质心可能落在多边形外(如凹多边形),
* 而内部点保证在几何内部。适用于标注、弹窗定位。
* </p>
*
* @param wkt 几何对象的 WKT 字符串
* @return 内部点坐标
*/
public static LatLng interiorPoint(String wkt) {
try {
Geometry geometry = new WKTReader().read(wkt);
Point point = geometry.getInteriorPoint();
return new LatLng(point.getX(), point.getY());
} catch (Exception e) {
throw new RuntimeException("内部点计算失败:" + e.getMessage(), e);
}
}
/**
* 计算几何对象的凸包(最小凸多边形)
* <p>
* 凸包是包含几何中所有点的最小凸多边形,相当于"最外层轮廓"。
* 常用于聚合分析、碰撞检测。
* </p>
*
* @param wkt 几何对象的 WKT 字符串
* @return 凸包的 WKT 字符串
*/
public static String convexHull(String wkt) {
try {
Geometry geometry = new WKTReader().read(wkt);
Geometry hull = new ConvexHull(geometry).getConvexHull();
return wktWriter.write(hull);
} catch (Exception e) {
throw new RuntimeException("凸包计算失败:" + e.getMessage(), e);
}
}
/**
* 计算最小外接矩形(Minimum Bounding Rectangle / Envelope)
*
* @param wkt 几何对象的 WKT 字符串
* @return 最小外接矩形的 WKT(Polygon)
*/
public static String minimumBoundingRectangle(String wkt) {
try {
Geometry geometry = new WKTReader().read(wkt);
Geometry envelope = geometry.getEnvelope();
return wktWriter.write(envelope);
} catch (Exception e) {
throw new RuntimeException("最小外接矩形计算失败:" + e.getMessage(), e);
}
}
/**
* 计算最小外接圆(Minimum Bounding Circle)
*
* @param wkt 几何对象的 WKT 字符串
* @return 外接圆的 WKT(Polygon)
*/
public static String minimumBoundingCircle(String wkt) {
try {
Geometry geometry = new WKTReader().read(wkt);
MinimumBoundingCircle mbc = new MinimumBoundingCircle(geometry);
Geometry circle = mbc.getCircle();
return wktWriter.write(circle);
} catch (Exception e) {
throw new RuntimeException("最小外接圆计算失败:" + e.getMessage(), e);
}
}
/**
* 计算几何对象的最小宽度(Minimum Diameter)
* <p>
* 最小宽度即几何对象最窄处的宽度,也称"宽度"或"厚度"。
* 可用于判断线状地理要素的粗细。
* </p>
*
* @param wkt 几何对象的 WKT 字符串
* @return 最小宽度的 WKT 线段
*/
public static String minimumDiameter(String wkt) {
try {
Geometry geometry = new WKTReader().read(wkt);
MinimumDiameter md = new MinimumDiameter(geometry);
Geometry diamLine = md.getDiameter();
return wktWriter.write(diamLine);
} catch (Exception e) {
throw new RuntimeException("最小宽度计算失败:" + e.getMessage(), e);
}
}
/**
* 计算包络矩形(Envelope)的四个角
*
* @param wkt 几何对象的 WKT 字符串
* @return [minLng, minLat, maxLng, maxLat]
*/
public static double[] envelope(String wkt) {
try {
Geometry geometry = new WKTReader().read(wkt);
Envelope env = geometry.getEnvelopeInternal();
return new double[]{env.getMinX(), env.getMinY(), env.getMaxX(), env.getMaxY()};
} catch (Exception e) {
throw new RuntimeException("包络矩形计算失败:" + e.getMessage(), e);
}
}
/**
* 计算两点之间的曼哈顿距离(经纬度差绝对值之和)
* <p>
* 曼哈顿距离 = |Δlng| + |Δlat|,常用于粗略估算、网格搜索。
* </p>
*
* @param lng1 点1 经度
* @param lat1 点1 纬度
* @param lng2 点2 经度
* @param lat2 点2 纬度
* @return 曼哈顿距离(单位:度)
*/
public static double manhattanDistance(double lng1, double lat1, double lng2, double lat2) {
return Math.abs(lng2 - lng1) + Math.abs(lat2 - lat1);
}
/**
* 判断多边形顶点的排列方向
* <p>
* 返回 true 表示顺时针(CW),false 表示逆时针(CCW)。
* 对于面数据,外环必须是逆时针,内环(洞)必须是顺时针,
* 否则部分 GIS 工具或数据库可能解析出错。
* </p>
*
* @param wkt 多边形 WKT
* @return true=顺时针,false=逆时针
*/
public static boolean isClockwise(String wkt) {
try {
Geometry geometry = new WKTReader().read(wkt);
if (geometry instanceof Polygon) {
Coordinate[] coords = ((Polygon) geometry).getExteriorRing().getCoordinates();
return Orientation.isCCW(coords);
}
return false;
} catch (Exception e) {
throw new RuntimeException("方向判断失败:" + e.getMessage(), e);
}
}
// ==================== 模块八:几何对象操作(合并、交集、差集、对称差) ====================
/**
* 几何对象的布尔运算:合并(Union)
* <p>
* 返回两个几何对象并集区域。
* 如果两个几何体相交,合并后为一个整体。
* </p>
*
* @param wktA 几何A 的 WKT
* @param wktB 几何B 的 WKT
* @return 合并结果的 WKT
*/
public static String union(String wktA, String wktB) {
try {
Geometry gA = new WKTReader().read(wktA);
Geometry gB = new WKTReader().read(wktB);
Geometry result = gA.union(gB);
return wktWriter.write(result);
} catch (Exception e) {
throw new RuntimeException("合并(Union)失败:" + e.getMessage(), e);
}
}
/**
* 批量合并多个几何对象
*
* @param wktList 多个 WKT 字符串
* @return 合并结果的 WKT
*/
public static String unionAll(List<String> wktList) {
try {
List<Geometry> geometries = new ArrayList<>();
for (String wkt : wktList) {
geometries.add(new WKTReader().read(wkt));
}
Geometry result = UnaryUnionOp.union(geometries);
return wktWriter.write(result);
} catch (Exception e) {
throw new RuntimeException("批量合并(UnaryUnion)失败:" + e.getMessage(), e);
}
}
/**
* 几何对象的布尔运算:交集(Intersection)
* <p>
* 返回两个几何对象的公共重叠区域。
* 若不重叠则返回空几何。
* </p>
*
* @param wktA 几何A 的 WKT
* @param wktB 几何B 的 WKT
* @return 交集结果的 WKT
*/
public static String intersection(String wktA, String wktB) {
try {
Geometry gA = new WKTReader().read(wktA);
Geometry gB = new WKTReader().read(wktB);
Geometry result = gA.intersection(gB);
return wktWriter.write(result);
} catch (Exception e) {
throw new RuntimeException("交集(Intersection)失败:" + e.getMessage(), e);
}
}
/**
* 几何对象的布尔运算:差集(Difference)
* <p>
* 返回几何A中减去几何B剩余的部分(A - B)。
* </p>
*
* @param wktA 几何A 的 WKT(被减数)
* @param wktB 几何B 的 WKT(减数)
* @return 差集结果的 WKT
*/
public static String difference(String wktA, String wktB) {
try {
Geometry gA = new WKTReader().read(wktA);
Geometry gB = new WKTReader().read(wktB);
Geometry result = gA.difference(gB);
return wktWriter.write(result);
} catch (Exception e) {
throw new RuntimeException("差集(Difference)失败:" + e.getMessage(), e);
}
}
/**
* 几何对象的布尔运算:对称差(SymDifference)
* <p>
* 返回两个几何对象中不重叠的部分(A∪B - A∩B)。
* 即两个几何合并后去掉重叠区域。
* </p>
*
* @param wktA 几何A 的 WKT
* @param wktB 几何B 的 WKT
* @return 对称差结果的 WKT
*/
public static String symDifference(String wktA, String wktB) {
try {
Geometry gA = new WKTReader().read(wktA);
Geometry gB = new WKTReader().read(wktB);
Geometry result = gA.symDifference(gB);
return wktWriter.write(result);
} catch (Exception e) {
throw new RuntimeException("对称差(SymDifference)失败:" + e.getMessage(), e);
}
}
// ==================== 模块九:几何验证 ====================
/**
* 几何验证结果
*/
public static class ValidationResult {
/** 几何是否有效 */
public final boolean valid;
/** 验证错误信息(有效时为空字符串) */
public final String error;
public ValidationResult(boolean valid, String error) {
this.valid = valid;
this.error = error;
}
}
/**
* 验证几何对象的有效性
* <p>
* 根据 OGC Simple Feature Access 标准检查:
* <ul>
* <li>自相交</li>
* <li>环的方向</li>
* <li>坐标异常(NaN、Infinity)</li>
* <li>空几何</li>
* </ul>
* </p>
*
* @param wkt 待验证的 WKT 字符串
* @return 验证结果
*/
public static ValidationResult validate(String wkt) {
try {
Geometry geometry = new WKTReader().read(wkt);
IsValidOp isValidOp = new IsValidOp(geometry);
boolean valid = isValidOp.isValid();
String error = valid ? "" : (isValidOp.getValidationError() != null
? isValidOp.getValidationError().getMessage() : "未知错误");
return new ValidationResult(valid, error);
} catch (Exception e) {
return new ValidationResult(false, "WKT 解析失败: " + e.getMessage());
}
}
/**
* 判断几何对象是否简单(无自相交)
* <p>
* 对于线串,简单表示没有自相交点。
* 对于面,简单表示没有自相交环。
* </p>
*
* @param wkt 几何 WKT
* @return 简单返回 true
*/
public static boolean isSimple(String wkt) {
try {
Geometry geometry = new WKTReader().read(wkt);
return geometry.isSimple();
} catch (Exception e) {
throw new RuntimeException("简单性判断失败:" + e.getMessage(), e);
}
}
/**
* 判断几何对象是否为空
*
* @param wkt 几何 WKT
* @return 空几何返回 true
*/
public static boolean isEmpty(String wkt) {
try {
Geometry geometry = new WKTReader().read(wkt);
return geometry.isEmpty();
} catch (Exception e) {
throw new RuntimeException("空判断失败:" + e.getMessage(), e);
}
}
/**
* 判断几何是否为矩形
*
* @param wkt 几何 WKT
* @return 矩形返回 true
*/
public static boolean isRectangle(String wkt) {
try {
Geometry geometry = new WKTReader().read(wkt);
return geometry.isRectangle();
} catch (Exception e) {
throw new RuntimeException("矩形判断失败:" + e.getMessage(), e);
}
}
// ==================== 模块十:几何简化、平滑与修复 ====================
/**
* Douglas-Peucker 算法简化线串(保留主要形状)
* <p>
* 通过移除"不重要"的顶点来减少点数,保留线串的总体形状。
* 适用于大数据量路线的轻量化展示。
* </p>
* <p>
* 算法原理:递归地找离线段最远的点,如果距离小于容差则移除中间点。
* </p>
*
* @param wkt 待简化的几何 WKT
* @param toleranceMeters 简化容差(单位:米),越大简化程度越高
* @return 简化后的 WKT
*/
public static String simplifyDP(String wkt, double toleranceMeters) {
try {
Geometry geometry = new WKTReader().read(wkt);
double toleranceDegrees = toleranceMeters / 111320.0;
Geometry simplified = DouglasPeuckerSimplifier.simplify(geometry, toleranceDegrees);
return wktWriter.write(simplified);
} catch (Exception e) {
throw new RuntimeException("Douglas-Peucker 简化失败:" + e.getMessage(), e);
}
}
/**
* 拓扑保持简化(保证简化后不会产生自相交等拓扑错误)
* <p>
* 在 Douglas-Peucker 基础上加入拓扑约束,适合需要保持几何有效性的场景。
* </p>
*
* @param wkt 待简化的几何 WKT
* @param toleranceMeters 简化容差(单位:米)
* @return 简化后的 WKT
*/
public static String simplifyTopologyPreserving(String wkt, double toleranceMeters) {
try {
Geometry geometry = new WKTReader().read(wkt);
double toleranceDegrees = toleranceMeters / 111320.0;
Geometry simplified = TopologyPreservingSimplifier.simplify(geometry, toleranceDegrees);
return wktWriter.write(simplified);
} catch (Exception e) {
throw new RuntimeException("拓扑保持简化失败:" + e.getMessage(), e);
}
}
/**
* Visvalingam-Whyatt 算法简化(基于面积阈值)
* <p>
* 与 Douglas-Peucker 不同,VW 算法基于三角形面积移除顶点,
* 对线串的弯曲程度保留更好。
* </p>
*
* @param wkt 待简化的几何 WKT
* @param areaToleranceDeg 面积容差(单位:平方度),越大简化程度越高
* @return 简化后的 WKT
*/
public static String simplifyVW(String wkt, double areaToleranceDeg) {
try {
Geometry geometry = new WKTReader().read(wkt);
Geometry simplified = VWSimplifier.simplify(geometry, areaToleranceDeg);
return wktWriter.write(simplified);
} catch (Exception e) {
throw new RuntimeException("VW 简化失败:" + e.getMessage(), e);
}
}
/**
* 合并相邻的线串(LineMerge)
* <p>
* 将一组端点相连的线串合并为更长的线串,
* 适用于路网拓扑连接。
* </p>
*
* @param wktList 待合并的多个线串 WKT
* @return 合并后的 WKT(MultipleLineString)
*/
public static String mergeLines(List<String> wktList) {
try {
LineMerger merger = new LineMerger();
for (String wkt : wktList) {
merger.add(new WKTReader().read(wkt));
}
Collection<?> merged = merger.getMergedLineStrings();
if (merged.isEmpty()) {
return null;
}
// 将 Collection 转为 MultiLineString
LineString[] lines = merged.toArray(new LineString[0]);
if (lines.length == 1) {
return wktWriter.write(lines[0]);
}
return wktWriter.write(geometryFactory.createMultiLineString(lines));
} catch (Exception e) {
throw new RuntimeException("线串合并失败:" + e.getMessage(), e);
}
}
/**
* 将面拆分为多个不相交的面(Polygonizer)
* <p>
* Polygonizer 可以将一组相交的线串识别并构建为多边形。
* 适用于从路网中提取街区、地块等。
* </p>
*
* @param wkt 待处理的线串 WKT
* @return 多边形列表的 WKT(每个多边形独立)
*/
public static List<String> polygonize(String wkt) {
try {
Geometry geometry = new WKTReader().read(wkt);
Polygonizer polygonizer = new Polygonizer();
polygonizer.add(geometry);
@SuppressWarnings("unchecked")
Collection<Polygon> polygons = polygonizer.getPolygons();
List<String> result = new ArrayList<>();
for (Polygon poly : polygons) {
result.add(wktWriter.write(poly));
}
return result;
} catch (Exception e) {
throw new RuntimeException("面化(Polygonizer)失败:" + e.getMessage(), e);
}
}
/**
* 从几何中提取所有线串
* <p>
* 使用递归遍历 geometry 树,收集所有 LineString 类型的子几何。
* 支持嵌套的 GeometryCollection 和各类 Multi* 几何。
* </p>
*
* @param wkt 几何 WKT
* @return 线串 WKT 列表
*/
public static List<String> extractLineStrings(String wkt) {
try {
Geometry geometry = new WKTReader().read(wkt);
List<LineString> lines = new ArrayList<>();
collectLineStrings(geometry, lines);
List<String> result = new ArrayList<>();
for (LineString line : lines) {
result.add(wktWriter.write(line));
}
return result;
} catch (Exception e) {
throw new RuntimeException("提取线串失败:" + e.getMessage(), e);
}
}
/**
* 递归遍历几何对象,收集所有 LineString
* <p>
* 算法:如果当前对象是 LineString 直接收集;如果是几何集合则递归遍历每个子元素。
* Point、Polygon 等非线串类型自动跳过。
* </p>
*
* @param geom 当前遍历的几何对象
* @param result 收集结果列表
*/
private static void collectLineStrings(Geometry geom, List<LineString> result) {
if (geom instanceof LineString) {
result.add((LineString) geom);
} else if (geom instanceof GeometryCollection) {
for (int i = 0; i < geom.getNumGeometries(); i++) {
collectLineStrings(geom.getGeometryN(i), result);
}
}
// Point、Polygon 等其他类型不是 LineString,不做处理
}
/**
* 从几何中提取所有面
*
* @param wkt 几何 WKT
* @return 面 WKT 列表
*/
public static List<String> extractPolygons(String wkt) {
try {
Geometry geometry = new WKTReader().read(wkt);
@SuppressWarnings("unchecked")
List<Polygon> polygons = PolygonExtracter.getPolygons(geometry);
List<String> result = new ArrayList<>();
for (Polygon poly : polygons) {
result.add(wktWriter.write(poly));
}
return result;
} catch (Exception e) {
throw new RuntimeException("提取面失败:" + e.getMessage(), e);
}
}
// ==================== 模块十一:空间筛选与过滤 ====================
/**
* 从一组经纬度点中筛选出在指定圆形范围(半径)内的点
*
* @param points 待筛选的点列表
* @param centerLng 圆心经度
* @param centerLat 圆心纬度
* @param radiusMeters 半径(单位:米)
* @return 在圆内的点列表
*/
public static List<LatLng> filterByCircle(List<LatLng> points,
double centerLng, double centerLat,
double radiusMeters) {
List<LatLng> result = new ArrayList<>();
for (LatLng p : points) {
if (haversineDistance(centerLng, centerLat, p.lng, p.lat) <= radiusMeters) {
result.add(p);
}
}
return result;
}
/**
* 从一组经纬度点中筛选出在指定多边形内的点
*
* @param points 待筛选的点列表
* @param polygonWkt 多边形的 WKT 字符串
* @return 在多边形内的点列表
*/
public static List<LatLng> filterByPolygon(List<LatLng> points, String polygonWkt) {
try {
Geometry polygon = new WKTReader().read(polygonWkt);
List<LatLng> result = new ArrayList<>();
for (LatLng p : points) {
Geometry point = geometryFactory.createPoint(new Coordinate(p.lng, p.lat));
if (polygon.contains(point)) {
result.add(p);
}
}
return result;
} catch (Exception e) {
throw new RuntimeException("多边形筛选失败:" + e.getMessage(), e);
}
}
/**
* 从一组经纬度中按距离排序(由近到远)
*
* @param points 待排序的经纬度列表
* @param refLng 参考点经度
* @param refLat 参考点纬度
* @param limit 返回条数上限(<=0 表示不限制)
* @return 按距离升序排列后的点列表
*/
public static List<LatLng> sortByDistance(List<LatLng> points,
double refLng, double refLat,
int limit) {
// 计算每个点的距离并包装
List<LatLng> sorted = new ArrayList<>(points);
sorted.sort(Comparator.comparingDouble(p -> haversineDistance(refLng, refLat, p.lng, p.lat)));
if (limit > 0 && sorted.size() > limit) {
return sorted.subList(0, limit);
}
return sorted;
}
// ==================== 模块十二:通用辅助 ====================
/**
* 将精度为 double 的坐标保留指定小数位数(四舍五入)
*
* @param value 原始值
* @param scale 小数位数
* @return 保留指定位数后的值
*/
public static double roundTo(double value, int scale) {
return BigDecimal.valueOf(value)
.setScale(scale, RoundingMode.HALF_UP)
.doubleValue();
}
/**
* 将坐标列表转为 WKT 线串
*
* @param points 经纬度点列表
* @return LINESTRING WKT 字符串
*/
public static String toLineStringWkt(List<LatLng> points) {
if (points == null || points.size() < 2) {
throw new IllegalArgumentException("至少需要 2 个点才能构成线串");
}
StringBuilder sb = new StringBuilder("LINESTRING (");
for (int i = 0; i < points.size(); i++) {
if (i > 0) sb.append(", ");
sb.append(points.get(i).lng).append(" ").append(points.get(i).lat);
}
sb.append(")");
return sb.toString();
}
/**
* 解析 GeoJSON 中支持多类型(扩展原有 geoJsonToWkt 只支持 LineString 的限制)
* <p>
* 此方法不修改原有 geoJsonToWkt 方法,而是作为全类型支持的独立方法添加。
* 支持:Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, GeometryCollection
* </p>
*
* @param geoJson GeoJSON 字符串
* @return WKT 字符串
*/
public static String geoJsonToWktFull(String geoJson) {
if (StrUtil.isBlank(geoJson)) {
return null;
}
try {
JsonNode root = objectMapper.readTree(geoJson);
return wktWriter.write(parseGeoJsonGeometry(root));
} catch (Exception e) {
throw new RuntimeException("GeoJSON 全类型转换 WKT 失败:" + e.getMessage(), e);
}
}
/**
* 递归解析 GeoJSON 节点为 JTS Geometry
*/
private static Geometry parseGeoJsonGeometry(JsonNode node) {
if (node == null) {
return null;
}
String type = node.get("type").asText();
switch (type.toUpperCase()) {
case "POINT":
return geometryFactory.createPoint(parseGeoJsonCoordinate(node.get("coordinates")));
case "LINESTRING":
return geometryFactory.createLineString(parseGeoJsonCoordinateArray(node.get("coordinates")));
case "POLYGON":
return parseGeoJsonPolygon(node.get("coordinates"));
case "MULTIPOINT": {
ArrayNode coords = (ArrayNode) node.get("coordinates");
Point[] pts = new Point[coords.size()];
for (int i = 0; i < coords.size(); i++) {
pts[i] = geometryFactory.createPoint(parseGeoJsonCoordinate(coords.get(i)));
}
return geometryFactory.createMultiPoint(pts);
}
case "MULTILINESTRING": {
ArrayNode lines = (ArrayNode) node.get("coordinates");
LineString[] lss = new LineString[lines.size()];
for (int i = 0; i < lines.size(); i++) {
lss[i] = geometryFactory.createLineString(parseGeoJsonCoordinateArray(lines.get(i)));
}
return geometryFactory.createMultiLineString(lss);
}
case "MULTIPOLYGON": {
ArrayNode polys = (ArrayNode) node.get("coordinates");
Polygon[] pgs = new Polygon[polys.size()];
for (int i = 0; i < polys.size(); i++) {
pgs[i] = parseGeoJsonPolygon(polys.get(i));
}
return geometryFactory.createMultiPolygon(pgs);
}
case "GEOMETRYCOLLECTION": {
ArrayNode geoms = (ArrayNode) node.get("geometries");
Geometry[] gs = new Geometry[geoms.size()];
for (int i = 0; i < geoms.size(); i++) {
gs[i] = parseGeoJsonGeometry(geoms.get(i));
}
return geometryFactory.createGeometryCollection(gs);
}
default:
throw new RuntimeException("不支持的 GeoJSON 类型: " + type);
}
}
private static Coordinate parseGeoJsonCoordinate(JsonNode node) {
return new Coordinate(node.get(0).asDouble(), node.get(1).asDouble());
}
private static Coordinate[] parseGeoJsonCoordinateArray(JsonNode node) {
Coordinate[] coords = new Coordinate[node.size()];
for (int i = 0; i < node.size(); i++) {
coords[i] = parseGeoJsonCoordinate(node.get(i));
}
return coords;
}
private static Polygon parseGeoJsonPolygon(JsonNode ringsNode) {
LinearRing shell = geometryFactory.createLinearRing(
parseGeoJsonCoordinateArray(ringsNode.get(0)));
LinearRing[] holes = new LinearRing[ringsNode.size() - 1];
for (int i = 1; i < ringsNode.size(); i++) {
holes[i - 1] = geometryFactory.createLinearRing(
parseGeoJsonCoordinateArray(ringsNode.get(i)));
}
return geometryFactory.createPolygon(shell, holes);
}
}
调用示例:
java
package cn.jky.yanghu.module.data.utils;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.io.WKTReader;
import org.locationtech.jts.io.WKTWriter;
import java.util.Arrays;
import java.util.List;
/**
* =====================================================================
* GeoSpatialUtil 工具类 ------ 完整方法调用示例
* =====================================================================
*
* 【目的】
* 本 Demo 用于演示 GeoSpatialUtil 中每一个 public 方法的用法。
* 所有测试数据均采用广州 / 广东地区的真实地理坐标,方便理解。
*
* 【如何运行】
* 1. 确保项目已正确导入 jts-core、jackson、hutool 等依赖
* 2. 直接运行 main 方法,控制台会打印每一步的输入、输出和说明
* 3. 建议用 IntelliJ 打开,按左侧行号旁的 ▶ 运行
*
* 【测试数据说明】
* - 广州塔(Canton Tower):113.324568, 23.106682
* - 天河体育中心:113.330000, 23.135000
* - 珠江新城:113.320000, 23.120000
* - G107 国道某段:起点(113.100, 23.000) → 终点(113.200, 23.100)
* - 广州市区大致范围:113.20~113.40, 23.00~23.20
*
* 【作者】GeoSpatialUtil Demo
* =====================================================================
*/
public class GeoSpatialUtilDemo {
// =====================================================================
// 工具对象:供后面部分方法使用
// =====================================================================
private static final GeometryFactory geometryFactory = new GeometryFactory();
private static final WKTReader wktReader = new WKTReader();
private static final WKTWriter wktWriter = new WKTWriter();
public static void main(String[] args) throws Exception {
// =============================================================
// 第〇部分:数据准备 ------ 先定义一些常用的坐标和几何,后面反复用到
// =============================================================
System.out.println("╔══════════════════════════════════════════════════════════╗");
System.out.println("║ GeoSpatialUtil 方法调用全演示 ║");
System.out.println("╚══════════════════════════════════════════════════════════╝");
System.out.println();
// ---- 基础坐标点 ----
double gzLng = 113.324568; // 广州塔 经度
double gzLat = 23.106682; // 广州塔 纬度
double tyLng = 113.330000; // 天河体育中心 经度
double tyLat = 23.135000; // 天河体育中心 纬度
double zjLng = 113.320000; // 珠江新城 经度
double zjLat = 23.120000; // 珠江新城 纬度
// ---- 路线相关数据(G107 国道某段)----
double roadAx = 113.100, roadAy = 23.000; // 路线起点 A
double roadBx = 113.200, roadBy = 23.100; // 路线终点 B
double qdzh = 1000.0; // 起点桩号 1000 公里
double zdzh = 1050.0; // 终点桩号 1050 公里
// ---- WKT 字符串(数据库常用的空间格式)----
String lineWkt = "LINESTRING (113.1 23.0, 113.15 23.05, 113.2 23.1)";
String polygonWkt = "POLYGON ((113.2 23.0, 113.4 23.0, 113.4 23.2, 113.2 23.2, 113.2 23.0))";
String pointWkt = "POINT (113.324568 23.106682)";
// ---- GeoJSON 字符串(前端常用的空间格式,如 Leaflet 拖出来的)----
String geoJsonLine = "{\"type\":\"LineString\",\"coordinates\":[[113.1,23.0],[113.15,23.05],[113.2,23.1]]}";
String geoJsonPoint = "{\"type\":\"Point\",\"coordinates\":[113.324568,23.106682]}";
// ---- 用 WKT 构造 JTS 对象(供某些需要 Geometry 参数的方法)----
LineString jtsLine = (LineString) wktReader.read(lineWkt);
Point gzPoint = geometryFactory.createPoint(new Coordinate(gzLng, gzLat));
// ---- 点列表(用于筛选、排序测试)----
GeoSpatialUtil.LatLng p1 = new GeoSpatialUtil.LatLng(gzLng, gzLat); // 广州塔
GeoSpatialUtil.LatLng p2 = new GeoSpatialUtil.LatLng(tyLng, tyLat); // 天河体育中心
GeoSpatialUtil.LatLng p3 = new GeoSpatialUtil.LatLng(zjLng, zjLat); // 珠江新城
GeoSpatialUtil.LatLng p4 = new GeoSpatialUtil.LatLng(113.400, 23.080); // 广州东附近
GeoSpatialUtil.LatLng p5 = new GeoSpatialUtil.LatLng(113.500, 23.300); // 增城区(较远)
List<GeoSpatialUtil.LatLng> points = Arrays.asList(p1, p2, p3, p4, p5);
// =============================================================
// 第一部分:GeoJSON ↔ WKT 互转(最常用,前端与数据库之间的桥梁)
// =============================================================
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
System.out.println("【第一部分】GeoJSON 与 WKT 互转");
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
System.out.println();
// ---- 1.1 geoJsonToWkt ----
// 功能:把前端传来的 GeoJSON 字符串转成达梦数据库能存的 WKT 格式
// 场景:前端 Leaflet 拖出一条路线,后端收到 GeoJSON,存入库
System.out.println("▶ 1.1 geoJsonToWkt");
System.out.println(" 输入(GeoJSON):" + geoJsonLine);
String wktResult = GeoSpatialUtil.geoJsonToWkt(geoJsonLine);
System.out.println(" 输出(WKT): " + wktResult);
System.out.println(" 说明:前端传来的路线 JSON → 达梦数据库 LINESTRING");
System.out.println();
// ---- 1.2 wktToGeoJson ----
// 功能:把数据库里的 WKT 转回 GeoJSON 给前端展示
// 场景:从库查出的路线 WKT,返回给前端 Leaflet 渲染
System.out.println("▶ 1.2 wktToGeoJson");
System.out.println(" 输入(WKT):" + lineWkt);
String geoJsonResult = GeoSpatialUtil.wktToGeoJson(lineWkt);
System.out.println(" 输出(GeoJSON):" + geoJsonResult);
System.out.println(" 说明:达梦数据库 LINESTRING → 前端 JSON 渲染");
System.out.println();
// ---- 1.3 geoJsonToWktFull ----
// 功能:与 geoJsonToWkt 相同,但支持所有 GeoJSON 类型(Point、Polygon 等)
// 原有 geoJsonToWkt 只支持 LineString,这个是全类型版
System.out.println("▶ 1.3 geoJsonToWktFull(全类型支持)");
System.out.println(" 输入(GeoJSON Point):" + geoJsonPoint);
String fullResult = GeoSpatialUtil.geoJsonToWktFull(geoJsonPoint);
System.out.println(" 输出(WKT):" + fullResult);
System.out.println(" 说明:支持 Point/Polygon/Multi* 等全部 GeoJSON 类型");
System.out.println();
// =============================================================
// 第二部分:坐标系统转换(WGS84 ↔ GCJ02 ↔ CGCS2000)
// =============================================================
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
System.out.println("【第二部分】坐标系统转换");
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
System.out.println(" 注:WGS84=GPS原始 GCJ02=火星(高德/腾讯) CGCS2000=国家2000");
System.out.println();
// ---- 2.1 isInChina ----
// 功能:判断经纬度是否在中国境内(中国境外不需要 GCJ02 偏移)
System.out.println("▶ 2.1 isInChina");
System.out.println(" 广州塔 (113.32, 23.10) → " + GeoSpatialUtil.isInChina(gzLng, gzLat) + "(在中国)");
System.out.println(" 东京 (139.69, 35.69) → " + GeoSpatialUtil.isInChina(139.69, 35.69) + "(不在中国)");
System.out.println();
// ---- 2.2 convertCoord ----
// 功能:通用坐标转换入口,通过 from/to 参数指定转换方向
System.out.println("▶ 2.2 convertCoord(通用转换)");
GeoSpatialUtil.LatLng converted = GeoSpatialUtil.convertCoord(
gzLng, gzLat,
GeoSpatialUtil.CoordSystem.WGS84,
GeoSpatialUtil.CoordSystem.GCJ02);
System.out.println(" WGS84 → GCJ02: (" + gzLng + ", " + gzLat + ") → (" + converted.lng + ", " + converted.lat + ")");
System.out.println();
// ---- 2.3 wgs84ToGcj02 ----
// 功能:GPS 原始坐标 → 高德/腾讯地图可用的坐标
System.out.println("▶ 2.3 wgs84ToGcj02");
GeoSpatialUtil.LatLng gcj = GeoSpatialUtil.wgs84ToGcj02(gzLng, gzLat);
System.out.println(" 广州塔 WGS84 (" + gzLng + ", " + gzLat + ")");
System.out.println(" → GCJ02 (" + gcj.lng + ", " + gcj.lat + ")");
System.out.println(" 说明:高德/腾讯地图拾取的坐标就是 GCJ02,与 GPS 有几百米偏移");
System.out.println();
// ---- 2.4 gcj02ToWgs84 ----
// 功能:高德/腾讯地图拾取的坐标 → GPS 原始坐标
System.out.println("▶ 2.4 gcj02ToWgs84");
GeoSpatialUtil.LatLng wgs = GeoSpatialUtil.gcj02ToWgs84(gcj.lng, gcj.lat);
System.out.println(" GCJ02 (" + gcj.lng + ", " + gcj.lat + ")");
System.out.println(" → WGS84 (" + wgs.lng + ", " + wgs.lat + ")");
System.out.println(" 逆转换回去应该和原始广州塔坐标一致(迭代逼近,误差 <0.1m)");
System.out.println();
// ---- 2.5 convertCoords ----
// 功能:批量坐标转换(适合多个点一起转)
System.out.println("▶ 2.5 convertCoords(批量转换)");
double[][] coords = {{gzLng, gzLat}, {tyLng, tyLat}, {zjLng, zjLat}};
double[][] convertedCoords = GeoSpatialUtil.convertCoords(coords,
GeoSpatialUtil.CoordSystem.WGS84,
GeoSpatialUtil.CoordSystem.GCJ02);
System.out.println(" 批量转 GCJ02 结果:");
for (int i = 0; i < convertedCoords.length; i++) {
System.out.println(" [" + i + "] (" + convertedCoords[i][0] + ", " + convertedCoords[i][1] + ")");
}
System.out.println();
// =============================================================
// 第三部分:球面距离与度量计算
// =============================================================
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
System.out.println("【第三部分】球面距离与度量计算");
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
System.out.println();
// ---- 3.1 haversineDistance(4 个 double 参数版)----
// 功能:计算两个经纬度之间的球面最短距离(大圆距离)
// 算法:Haversine 公式,精度约 0.5%
System.out.println("▶ 3.1 haversineDistance(经纬度版)");
double dist = GeoSpatialUtil.haversineDistance(gzLng, gzLat, tyLng, tyLat);
System.out.println(" 广州塔 → 天河体育中心 直线距离 ≈ " + String.format("%.1f", dist) + " 米");
System.out.println(" 约 " + String.format("%.2f", dist / 1000) + " 公里");
System.out.println();
// ---- 3.2 haversineDistance(LatLng 对象版)----
System.out.println("▶ 3.2 haversineDistance(LatLng 对象版)");
double dist2 = GeoSpatialUtil.haversineDistance(p1, p2);
System.out.println(" 同上,用对象调: " + String.format("%.1f", dist2) + " 米");
System.out.println();
// ---- 3.3 totalDistance ----
// 功能:计算多段线的累计长度(广州塔→天河体育中心→珠江新城)
System.out.println("▶ 3.3 totalDistance(多点累计距离)");
List<GeoSpatialUtil.LatLng> route = Arrays.asList(p1, p2, p3);
double totalDist = GeoSpatialUtil.totalDistance(route);
System.out.println(" 广州塔 → 天河体育中心 → 珠江新城 折线总长:" + String.format("%.1f", totalDist) + " 米");
System.out.println();
// ---- 3.4 lineStringLength ----
// 功能:计算 JTS LineString 的球面长度
System.out.println("▶ 3.4 lineStringLength");
double lineLen = GeoSpatialUtil.lineStringLength(jtsLine);
System.out.println(" G107 路段 (" + lineWkt + ") 球面长度:" + String.format("%.1f", lineLen) + " 米");
System.out.println();
// ---- 3.5 bearing ----
// 功能:计算从 A 到 B 的方向角(0°=正北,顺时针)
System.out.println("▶ 3.5 bearing(方位角)");
double b = GeoSpatialUtil.bearing(gzLng, gzLat, tyLng, tyLat);
System.out.println(" 从广州塔指向天河体育中心,方向角 ≈ " + String.format("%.1f", b) + "°(0=正北)");
System.out.println();
// ---- 3.6 destinationPoint ----
// 功能:从起点沿某个方向走一段距离,算终点在哪
// 场景:已知当前位置和朝向,推算前方某点的坐标
System.out.println("▶ 3.6 destinationPoint(航位推算)");
GeoSpatialUtil.LatLng dest = GeoSpatialUtil.destinationPoint(gzLng, gzLat, 45, 1000);
System.out.println(" 从广州塔向东北方向(45°)走 1000 米,到达:("
+ String.format("%.6f", dest.lng) + ", " + String.format("%.6f", dest.lat) + ")");
System.out.println();
// ---- 3.7 midpoint ----
// 功能:计算两点连线的中间点
System.out.println("▶ 3.7 midpoint(中点)");
GeoSpatialUtil.LatLng mid = GeoSpatialUtil.midpoint(gzLng, gzLat, tyLng, tyLat);
System.out.println(" 广州塔→天河体育中心 的中点:("
+ String.format("%.6f", mid.lng) + ", " + String.format("%.6f", mid.lat) + ")");
System.out.println();
// ---- 3.8 manhattanDistance ----
// 功能:曼哈顿距离 = 经度差绝对值 + 纬度差绝对值,用于粗略估算
System.out.println("▶ 3.8 manhattanDistance(曼哈顿距离)");
double manhattan = GeoSpatialUtil.manhattanDistance(gzLng, gzLat, tyLng, tyLat);
System.out.println(" 广州塔→天河体育中心 曼哈顿距离:" + String.format("%.6f", manhattan) + " 度");
System.out.println();
// =============================================================
// 第四部分:点到线的空间分析(道路桩号计算的核心)
// =============================================================
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
System.out.println("【第四部分】点到线的空间分析");
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
System.out.println(" 场景:给出一个 GPS 点,找它离哪条路最近、垂足在哪、比例多少");
System.out.println();
// ---- 4.1 pointToSegment ----
// 功能:计算点到线段的最短距离、垂足坐标、投影比例(向量法)
// 场景:判断 GPS 点离当前路段的哪一段最近
System.out.println("▶ 4.1 pointToSegment(点到线段)");
GeoSpatialUtil.PointToLineResult segResult = GeoSpatialUtil.pointToSegment(
gzLng, gzLat, // 查询点(广州塔)
roadAx, roadAy, // 路段起点 A
roadBx, roadBy); // 路段终点 B
System.out.println(" 广州塔 到 G107 路段 [(" + roadAx + "," + roadAy + ")→(" + roadBx + "," + roadBy + ")]");
System.out.println(" 最短距离:" + String.format("%.1f", segResult.distance) + " 米");
System.out.println(" 垂足坐标:(" + String.format("%.6f", segResult.projectionPoint.lng)
+ ", " + String.format("%.6f", segResult.projectionPoint.lat) + ")");
System.out.println(" 投影比例:" + String.format("%.4f", segResult.ratio) + "(0=起点,1=终点)");
System.out.println();
// ---- 4.2 pointToLineString ----
// 功能:点到折线(多个线段)的全局最近点,原理同上但遍历所有线段
// 场景:一条路有多个拐弯,找 GPS 点离整条路的最近处
System.out.println("▶ 4.2 pointToLineString(点到折线)");
GeoSpatialUtil.PointToLineResult lineResult = GeoSpatialUtil.pointToLineString(
new GeoSpatialUtil.LatLng(gzLng, gzLat), jtsLine);
System.out.println(" 广州塔 到 折线 " + lineWkt);
System.out.println(" 最近距离:" + String.format("%.1f", lineResult.distance) + " 米");
System.out.println(" 最近点:(" + String.format("%.6f", lineResult.projectionPoint.lng)
+ ", " + String.format("%.6f", lineResult.projectionPoint.lat) + ")");
System.out.println(" 全局比例:" + String.format("%.4f", lineResult.ratio));
System.out.println();
// ---- 4.3 distanceToGeometry ----
// 功能:JTS 精确计算点到任意几何的最短距离
System.out.println("▶ 4.3 distanceToGeometry(JTS 精确距离)");
double jtsDist = GeoSpatialUtil.distanceToGeometry(gzPoint, jtsLine);
System.out.println(" JTS 计算广州塔到折线的平面距离:" + String.format("%.6f", jtsDist) + "(度)");
System.out.println();
// ---- 4.4 nearestPoint ----
// 功能:从一组点中找离参考点最近的那个
// 场景:附近最近的服务区、加油站等
System.out.println("▶ 4.4 nearestPoint(最近点搜索)");
GeoSpatialUtil.LatLng nearest = GeoSpatialUtil.nearestPoint(points, gzLng, gzLat);
System.out.println(" 参考点:广州塔 (" + gzLng + ", " + gzLat + ")");
System.out.println(" 候选点共 " + points.size() + " 个");
System.out.println(" 最近点:(" + nearest.lng + ", " + nearest.lat + ")" + "(就是广州塔自身,距离 0)");
System.out.println();
// =============================================================
// 第五部分:沿线插值与桩号计算(道路里程定位)
// =============================================================
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
System.out.println("【第五部分】沿线插值与桩号计算");
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
System.out.println(" 场景:已知某路段起终点桩号,根据 GPS 定位算当前位置的桩号");
System.out.println();
// ---- 5.1 interpolateAlongLine ----
// 功能:在折线上按距离找点(从起点沿路线走多少米到哪)
// 场景:路线巡检,从起点走了 3 公里,问现在在哪
System.out.println("▶ 5.1 interpolateAlongLine(沿线距离插值)");
double walkDistance = 3000; // 走 3 公里
GeoSpatialUtil.LatLng walkPoint = GeoSpatialUtil.interpolateAlongLine(jtsLine, walkDistance);
System.out.println(" 在路线 " + lineWkt + " 上从起点走 " + walkDistance + " 米");
System.out.println(" 到达:(" + String.format("%.6f", walkPoint.lng)
+ ", " + String.format("%.6f", walkPoint.lat) + ")");
System.out.println();
// ---- 5.2 interpolateByRatio ----
// 功能:在折线上按比例找点(0=起点,0.5=中点,1=终点)
System.out.println("▶ 5.2 interpolateByRatio(沿线比例插值)");
GeoSpatialUtil.LatLng midPoint = GeoSpatialUtil.interpolateByRatio(jtsLine, 0.5);
System.out.println(" 路线中点(50%位置):(" + String.format("%.6f", midPoint.lng)
+ ", " + String.format("%.6f", midPoint.lat) + ")");
System.out.println();
// ---- 5.3 calcStakeValue ----
// 功能:根据投影比例计算桩号数值
// 桩号 = 起点桩号 + (终点桩号 - 起点桩号) × 比例
System.out.println("▶ 5.3 calcStakeValue(计算桩号数值)");
// 假设广州塔投影到 G107 某路段的比例就是上面 pointToLineString 算出的 ratio
double stakeVal = GeoSpatialUtil.calcStakeValue(qdzh, zdzh, lineResult.ratio);
System.out.println(" 路段桩号范围:K" + qdzh + " ~ K" + zdzh);
System.out.println(" 投影比例:" + String.format("%.4f", lineResult.ratio));
System.out.println(" 计算桩号值:" + String.format("%.3f", stakeVal) + " 公里");
System.out.println();
// ---- 5.4 formatStakeNo ----
// 功能:桩号数值 → 桩号字符串(如 1025.300 → "K1025+300")
System.out.println("▶ 5.4 formatStakeNo(格式化桩号)");
String formatted = GeoSpatialUtil.formatStakeNo(stakeVal);
System.out.println(" 桩号值 " + String.format("%.3f", stakeVal) + " → 格式化后 " + formatted);
System.out.println(" 说明:K 表示公里,+ 后面的 3 位数字表示米");
System.out.println();
// ---- 5.5 parseStakeNo ----
// 功能:桩号字符串 → 桩号数值(如 "K1025+300" → 1025.300)
System.out.println("▶ 5.5 parseStakeNo(解析桩号)");
double parsed = GeoSpatialUtil.parseStakeNo(formatted);
System.out.println(" " + formatted + " → 解析回数值 " + String.format("%.3f", parsed) + " 公里");
System.out.println();
// =============================================================
// 第六部分:空间关系判断
// =============================================================
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
System.out.println("【第六部分】空间关系判断(DE-9IM 模型)");
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
System.out.println();
// ---- 6.1 checkSpatialRelation(WKT 参数版)----
// 功能:判断两个几何之间的空间关系(相交、包含、相邻等 10 种)
// 场景:判断 GPS 点是否在某个行政区内、两条路是否相交等
System.out.println("▶ 6.1 checkSpatialRelation(WKT 参数版)");
boolean contains = GeoSpatialUtil.checkSpatialRelation(
polygonWkt, pointWkt,
GeoSpatialUtil.SpatialRelation.CONTAINS);
System.out.println(" 多边形:" + polygonWkt);
System.out.println(" 点: " + pointWkt);
System.out.println(" 多边形包含点?" + contains);
System.out.println();
// ---- 6.2 checkSpatialRelation(Geometry 参数版)----
System.out.println("▶ 6.2 checkSpatialRelation(Geometry 参数版)");
Geometry polyGeom = wktReader.read(polygonWkt);
Geometry ptGeom = wktReader.read(pointWkt);
boolean intersects = GeoSpatialUtil.checkSpatialRelation(
polyGeom, ptGeom,
GeoSpatialUtil.SpatialRelation.INTERSECTS);
System.out.println(" 几何相交?" + intersects);
System.out.println();
// ---- 6.3 isPointNearLine ----
// 功能:判断点是否在线的缓冲区范围内(邻近查询)
// 场景:查询某 GPS 点附近是否有国道
System.out.println("▶ 6.3 isPointNearLine(点是否在线附近)");
boolean nearLine = GeoSpatialUtil.isPointNearLine(gzLng, gzLat, lineWkt, 50000);
System.out.println(" 广州塔 距 G107 路段 50 公里范围内?" + nearLine);
System.out.println(" 用 100 米范围再试:" + GeoSpatialUtil.isPointNearLine(gzLng, gzLat, lineWkt, 100));
System.out.println();
// ---- 6.4 isPointInPolygon ----
// 功能:判断点在多边形内(行政区划、电子围栏)
// 场景:判断 GPS 点是否进入/离开某区域
System.out.println("▶ 6.4 isPointInPolygon(点在多边形内)");
boolean inPoly = GeoSpatialUtil.isPointInPolygon(gzLng, gzLat, polygonWkt);
System.out.println(" 广州塔 在广州市区矩形范围 " + polygonWkt + " 内?" + inPoly);
System.out.println();
// ---- 6.5 isInBoundingBox ----
// 功能:判断点是否在矩形范围内(更轻量的判断,比 isPointInPolygon 快)
// 场景:屏幕可视区域内的要素快速筛选
System.out.println("▶ 6.5 isInBoundingBox(矩形范围判断)");
boolean inBox = GeoSpatialUtil.isInBoundingBox(gzLng, gzLat, 113.0, 23.0, 114.0, 24.0);
System.out.println(" 广州塔 在 [113.0,23.0]~[114.0,24.0] 范围内?" + inBox);
System.out.println();
// ---- 6.6 boundingBoxesIntersect ----
// 功能:两个矩形是否相交(用于空间索引快速过滤)
System.out.println("▶ 6.6 boundingBoxesIntersect(矩形相交判断)");
boolean boxIntersect = GeoSpatialUtil.boundingBoxesIntersect(
113.0, 23.0, 114.0, 24.0, // 框1:广州市区
113.2, 23.0, 113.5, 23.5); // 框2:广州东部
System.out.println(" 两个矩形相交?" + boxIntersect);
System.out.println();
// =============================================================
// 第七部分:缓冲区分析
// =============================================================
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
System.out.println("【第七部分】缓冲区分析");
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
System.out.println(" 场景:路线周边 500 米范围有哪些设施、点在不在缓冲区内");
System.out.println();
// ---- 7.1 buffer(指定平滑度)----
// 功能:给几何加一个"胖圈",正数向外扩,负数向内缩
// 场景:路线两侧 50 米范围 = 道路红线范围
System.out.println("▶ 7.1 buffer(指定平滑度)");
String buffered1 = GeoSpatialUtil.buffer(lineWkt, 500, 8);
System.out.println(" 原路线:" + lineWkt);
System.out.println(" 缓冲区(500米):" + buffered1);
System.out.println(" 说明:quadrantSegments=8 表示每段弧用 8 条线段逼近,越大越平滑");
System.out.println();
// ---- 7.2 buffer(默认平滑度)----
System.out.println("▶ 7.2 buffer(默认平滑度)");
String buffered2 = GeoSpatialUtil.buffer(lineWkt, 100);
System.out.println(" 100 米缓冲区:" + buffered2);
System.out.println();
// ---- 7.3 isWithinBuffer ----
// 功能:判断点是否在几何对象的缓冲区内
System.out.println("▶ 7.3 isWithinBuffer(点在缓冲区判断)");
boolean within = GeoSpatialUtil.isWithinBuffer(gzLng, gzLat, lineWkt, 100000);
System.out.println(" 广州塔 在 G107 路段 100 公里缓冲区内?" + within);
System.out.println();
// =============================================================
// 第八部分:空间计算(面积、周长、质心、凸包等)
// =============================================================
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
System.out.println("【第八部分】空间计算");
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
System.out.println();
// ---- 8.1 area ----
// 功能:计算多边形面积(粗略换算)
// 场景:计算某行政区的面积
System.out.println("▶ 8.1 area(面积)");
double area = GeoSpatialUtil.area(polygonWkt);
System.out.println(" 多边形 " + polygonWkt);
System.out.println(" 近似面积:" + String.format("%.1f", area) + " 平方米(约 "
+ String.format("%.1f", area / 1000000) + " 平方公里)");
System.out.println();
// ---- 8.2 perimeter ----
// 功能:计算周长
System.out.println("▶ 8.2 perimeter(周长)");
double peri = GeoSpatialUtil.perimeter(polygonWkt);
System.out.println(" 近似周长:" + String.format("%.1f", peri) + " 米(约 "
+ String.format("%.1f", peri / 1000) + " 公里)");
System.out.println();
// ---- 8.3 centroid ----
// 功能:计算几何质心(几何中心位置)
// 场景:面状要素标注位置定位
System.out.println("▶ 8.3 centroid(质心)");
GeoSpatialUtil.LatLng center = GeoSpatialUtil.centroid(polygonWkt);
System.out.println(" 多边形质心:(" + String.format("%.6f", center.lng)
+ ", " + String.format("%.6f", center.lat) + ")");
System.out.println();
// ---- 8.4 interiorPoint ----
// 功能:计算内部点(保证在多边形内部,适合放标签)
System.out.println("▶ 8.4 interiorPoint(内部点)");
GeoSpatialUtil.LatLng interior = GeoSpatialUtil.interiorPoint(polygonWkt);
System.out.println(" 多边形内部点(用于标注定位):(" + String.format("%.6f", interior.lng)
+ ", " + String.format("%.6f", interior.lat) + ")");
System.out.println();
// ---- 8.5 convexHull ----
// 功能:几何凸包(最外层轮廓,类似"打包")
// 场景:多个离散点的覆盖范围
System.out.println("▶ 8.5 convexHull(凸包)");
String hull = GeoSpatialUtil.convexHull(polygonWkt);
System.out.println(" 多边形凸包:" + hull);
System.out.println();
// ---- 8.6 minimumBoundingRectangle ----
// 功能:最小外接矩形
System.out.println("▶ 8.6 minimumBoundingRectangle(最小外接矩形)");
String mbr = GeoSpatialUtil.minimumBoundingRectangle(polygonWkt);
System.out.println(" 多边形最小外接矩形:" + mbr);
System.out.println();
// ---- 8.7 minimumBoundingCircle ----
// 功能:最小外接圆
System.out.println("▶ 8.7 minimumBoundingCircle(最小外接圆)");
String mbc = GeoSpatialUtil.minimumBoundingCircle(polygonWkt);
System.out.println(" 多边形最小外接圆:" + mbc);
System.out.println();
// ---- 8.8 minimumDiameter ----
// 功能:最小宽度
System.out.println("▶ 8.8 minimumDiameter(最小宽度)");
String md = GeoSpatialUtil.minimumDiameter(polygonWkt);
System.out.println(" 多边形最小宽度线段:" + md);
System.out.println();
// ---- 8.9 envelope ----
// 功能:包络矩形角点
System.out.println("▶ 8.9 envelope(包络矩形)");
double[] env = GeoSpatialUtil.envelope(polygonWkt);
System.out.println(" 多边形包络矩形:[minLng=" + env[0] + ", minLat=" + env[1]
+ ", maxLng=" + env[2] + ", maxLat=" + env[3] + "]");
System.out.println();
// ---- 8.10 isClockwise ----
// 功能:判断多边形顶点方向(外环应逆时针,内环应顺时针)
System.out.println("▶ 8.10 isClockwise(顶点方向判断)");
boolean cw = GeoSpatialUtil.isClockwise(polygonWkt);
System.out.println(" 多边形外环逆时针?" + !cw + "(外环必须逆时针才符合 OGC 标准)");
System.out.println();
// =============================================================
// 第九部分:几何布尔运算
// =============================================================
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
System.out.println("【第九部分】几何布尔运算");
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
System.out.println();
String polyA = "POLYGON ((113.2 23.0, 113.3 23.0, 113.3 23.1, 113.2 23.1, 113.2 23.0))";
String polyB = "POLYGON ((113.25 23.05, 113.35 23.05, 113.35 23.15, 113.25 23.15, 113.25 23.05))";
// ---- 9.1 union ----
System.out.println("▶ 9.1 union(合并)");
String union = GeoSpatialUtil.union(polyA, polyB);
System.out.println(" A + B 合并:" + union);
System.out.println(" 说明:两个多边形重叠部分合并成一个整体");
System.out.println();
// ---- 9.2 unionAll ----
System.out.println("▶ 9.2 unionAll(批量合并)");
String unionAll = GeoSpatialUtil.unionAll(Arrays.asList(polyA, polyB));
System.out.println(" 批量合并结果:" + unionAll);
System.out.println();
// ---- 9.3 intersection ----
System.out.println("▶ 9.3 intersection(交集)");
String inter = GeoSpatialUtil.intersection(polyA, polyB);
System.out.println(" A ∩ B 交集:" + inter);
System.out.println(" 说明:两个多边形重叠的公共区域");
System.out.println();
// ---- 9.4 difference ----
System.out.println("▶ 9.4 difference(差集 A - B)");
String diff = GeoSpatialUtil.difference(polyA, polyB);
System.out.println(" A - B 差集:" + diff);
System.out.println(" 说明:多边形 A 减去与 B 重叠部分后剩下的区域");
System.out.println();
// ---- 9.5 symDifference ----
System.out.println("▶ 9.5 symDifference(对称差)");
String symDiff = GeoSpatialUtil.symDifference(polyA, polyB);
System.out.println(" A ⊕ B 对称差:" + symDiff);
System.out.println(" 说明:A∪B - A∩B,即两个几何合起来去掉重叠");
System.out.println();
// =============================================================
// 第十部分:几何验证
// =============================================================
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
System.out.println("【第十部分】几何验证");
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
System.out.println(" 场景:检查从数据库读出的 WKT 是否合法,避免后续计算出错");
System.out.println();
// ---- 10.1 validate ----
System.out.println("▶ 10.1 validate(有效性验证)");
GeoSpatialUtil.ValidationResult validResult = GeoSpatialUtil.validate(polygonWkt);
System.out.println(" 正常多边形验证:" + (validResult.valid ? "有效" : "无效"));
if (!validResult.valid) System.out.println(" 错误:" + validResult.error);
System.out.println(" 自相交多边形验证:" + GeoSpatialUtil.validate(
"POLYGON ((0 0, 1 1, 0 1, 1 0, 0 0))").valid + "(预期无效)");
System.out.println();
// ---- 10.2 isSimple ----
System.out.println("▶ 10.2 isSimple(简单几何判断)");
System.out.println(" 正常线串简单?" + GeoSpatialUtil.isSimple(lineWkt));
System.out.println();
// ---- 10.3 isEmpty ----
System.out.println("▶ 10.3 isEmpty(空几何判断)");
System.out.println(" 正常多边形为空?" + GeoSpatialUtil.isEmpty(polygonWkt));
System.out.println(" 空多边形:" + GeoSpatialUtil.isEmpty("POLYGON EMPTY"));
System.out.println();
// ---- 10.4 isRectangle ----
System.out.println("▶ 10.4 isRectangle(矩形判断)");
System.out.println(" 广州范围是矩形?" + GeoSpatialUtil.isRectangle(polygonWkt));
System.out.println();
// =============================================================
// 第十一部分:几何简化与修复
// =============================================================
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
System.out.println("【第十一部分】几何简化与修复");
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
System.out.println(" 场景:大量顶点的大路线简化后展示,减少前端渲染压力");
System.out.println();
String longLine = "LINESTRING (0 0, 0.01 0.01, 0.02 0.02, 0.03 0.03, 0.04 0.04, 0.05 0.05)";
// ---- 11.1 simplifyDP ----
System.out.println("▶ 11.1 simplifyDP(Douglas-Peucker 简化)");
String simpleDP = GeoSpatialUtil.simplifyDP(longLine, 1000);
System.out.println(" 原线串:" + longLine + "(6 个点)");
System.out.println(" 简化后:" + simpleDP + "(去掉小幅弯曲的点)");
System.out.println();
// ---- 11.2 simplifyTopologyPreserving ----
System.out.println("▶ 11.2 simplifyTopologyPreserving(拓扑保持简化)");
String simpleTP = GeoSpatialUtil.simplifyTopologyPreserving(longLine, 500);
System.out.println(" 拓扑保持简化结果:" + simpleTP);
System.out.println();
// ---- 11.3 simplifyVW ----
System.out.println("▶ 11.3 simplifyVW(VW 算法简化)");
String simpleVW = GeoSpatialUtil.simplifyVW(longLine, 0.0001);
System.out.println(" VW 简化结果:" + simpleVW);
System.out.println();
// ---- 11.4 mergeLines ----
// 功能:把多个端到端的短路线合并为一条长路线
System.out.println("▶ 11.4 mergeLines(线串合并)");
List<String> linesToMerge = Arrays.asList(
"LINESTRING (0 0, 1 1)",
"LINESTRING (1 1, 2 2)",
"LINESTRING (2 2, 3 3)");
String merged = GeoSpatialUtil.mergeLines(linesToMerge);
System.out.println(" 3 条短线:" + linesToMerge);
System.out.println(" 合并后:" + merged);
System.out.println(" 说明:端到端相连的短线合为一条长线");
System.out.println();
// ---- 11.5 polygonize ----
// 功能:从相交线串中提取出多边形面(路网→街区)
System.out.println("▶ 11.5 polygonize(面化)");
List<String> polygons = GeoSpatialUtil.polygonize(
"MULTILINESTRING ((0 0, 1 0, 1 1, 0 1, 0 0))");
System.out.println(" 线串面化结果:" + polygons);
System.out.println(" 说明:由封闭线串构建出多边形");
System.out.println();
// ---- 11.6 extractLineStrings ----
// 功能:从任意几何中提取所有 LineString 子元素
System.out.println("▶ 11.6 extractLineStrings(提取线串)");
List<String> extractedLines = GeoSpatialUtil.extractLineStrings(
"MULTILINESTRING ((0 0, 1 1), (2 2, 3 3))");
System.out.println(" 从 MultiLineString 提取的线串:" + extractedLines);
System.out.println();
// ---- 11.7 extractPolygons ----
// 功能:从任意几何中提取所有 Polygon 子元素
System.out.println("▶ 11.7 extractPolygons(提取面)");
List<String> extractedPolys = GeoSpatialUtil.extractPolygons(
"MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((2 2, 3 2, 3 3, 2 3, 2 2)))");
System.out.println(" 从 MultiPolygon 提取的面:" + extractedPolys);
System.out.println();
// =============================================================
// 第十二部分:空间筛选
// =============================================================
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
System.out.println("【第十二部分】空间筛选");
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
System.out.println(" 场景:从一堆点中筛选出符合条件的点(圆内、多边形内、按距离排序)");
System.out.println();
System.out.println(" 候选点列表:");
for (int i = 0; i < points.size(); i++) {
System.out.println(" [" + i + "] (" + points.get(i).lng + ", " + points.get(i).lat + ")");
}
System.out.println();
// ---- 12.1 filterByCircle ----
// 功能:筛选指定圆心、半径范围内的点
System.out.println("▶ 12.1 filterByCircle(圆形范围筛选)");
List<GeoSpatialUtil.LatLng> inCircle = GeoSpatialUtil.filterByCircle(
points, gzLng, gzLat, 10000); // 广州塔为中心,10 公里半径
System.out.println(" 以广州塔为中心、10 公里半径内,命中 " + inCircle.size() + " 个点:");
for (GeoSpatialUtil.LatLng p : inCircle) {
System.out.println(" (" + p.lng + ", " + p.lat + ") → 距离 "
+ String.format("%.0f", GeoSpatialUtil.haversineDistance(gzLng, gzLat, p.lng, p.lat)) + " 米");
}
System.out.println();
// ---- 12.2 filterByPolygon ----
// 功能:筛选在多边形内的点
System.out.println("▶ 12.2 filterByPolygon(多边形范围筛选)");
List<GeoSpatialUtil.LatLng> inPoly2 = GeoSpatialUtil.filterByPolygon(points, polygonWkt);
System.out.println(" 在广州市区范围 " + polygonWkt + " 内,命中 " + inPoly2.size() + " 个点");
System.out.println();
// ---- 12.3 sortByDistance ----
// 功能:按到参考点的距离由近到远排序,并可限制返回条数
System.out.println("▶ 12.3 sortByDistance(按距离排序)");
List<GeoSpatialUtil.LatLng> sorted = GeoSpatialUtil.sortByDistance(
points, gzLng, gzLat, 3); // 取最近的 3 个
System.out.println(" 以广州塔为参考,最近的 3 个点(由近到远):");
for (GeoSpatialUtil.LatLng p : sorted) {
System.out.println(" (" + p.lng + ", " + p.lat + ") → 距离 "
+ String.format("%.0f", GeoSpatialUtil.haversineDistance(gzLng, gzLat, p.lng, p.lat)) + " 米");
}
System.out.println();
// =============================================================
// 第十三部分:通用辅助方法
// =============================================================
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
System.out.println("【第十三部分】通用辅助方法");
System.out.println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
System.out.println();
// ---- 13.1 roundTo ----
// 功能:保留指定小数位数(四舍五入)
System.out.println("▶ 13.1 roundTo(保留小数位数)");
System.out.println(" 113.324568 保留 3 位小数:" + GeoSpatialUtil.roundTo(113.324568, 3));
System.out.println(" 23.106682 保留 4 位小数:" + GeoSpatialUtil.roundTo(23.106682, 4));
System.out.println();
// ---- 13.2 toLineStringWkt ----
// 功能:把一组 LatLng 点拼成 LINESTRING WKT 字符串
// 场景:将前端传来的多点手动拼成 WKT 入库
System.out.println("▶ 13.2 toLineStringWkt(拼 WKT 线串)");
String wktLine = GeoSpatialUtil.toLineStringWkt(Arrays.asList(
new GeoSpatialUtil.LatLng(113.0, 23.0),
new GeoSpatialUtil.LatLng(113.5, 23.5)));
System.out.println(" 两个点拼成线:" + wktLine);
System.out.println();
// =============================================================
// 结束
// =============================================================
System.out.println("╔══════════════════════════════════════════════════════════╗");
System.out.println("║ 演示结束!所有方法已全部调用 ║");
System.out.println("╚══════════════════════════════════════════════════════════╝");
}
}