JTS工具类以及调用demo示例

工具类

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("╚══════════════════════════════════════════════════════════╝");
    }
}
相关推荐
无心使然1 天前
Openlayers图层按需分层渲染到不同Canvas画布
前端·vue.js·gis
丷丩2 天前
MapLibre GL JS第35课:显示带地形高程(三维地形)的卫星影像
javascript·gis·map·mapbox·maplibre gl js
丷丩3 天前
MapLibre GL JS第25课:添加栅格瓦片源
开发语言·javascript·gis·mapbox·maplibre gl js
丷丩4 天前
MapLibre GL JS第29课:添加Canvas源
javascript·gis·map·mapbox·maplibre gl js
丷丩4 天前
MapLibre GL JS第21课:绘制GeoJSON点图标、注记
前端·javascript·gis·mapbox·maplibre gl js
丷丩4 天前
MapLibre GL JS第20课:更新GeoJSON多边形
前端·javascript·gis·mapbox·maplibre gl js
丷丩4 天前
MapLibre GL JS第33课:渲染世界副本
javascript·gis·map·mapbox·maplibre gl js
丷丩4 天前
MapLibre GL JS第31课:添加实时数据
javascript·gis·map·mapbox·maplibre gl js
丷丩4 天前
MapLibre GL JS第28课:PMTiles源和协议
javascript·gis·map·mapbox·maplibre gl js