JTS简单使用

前言

LocationTech JTS 拓扑套件(JTS)是一个开源的 Java 软件库,提供了平面几何的对象模型以及一套基础的几何函数。JTS 遵循开放 GIS 联盟发布的 SQL 简单要素规范(Simple Features Specification for SQL)。JTS旨在作为基于矢量的地理信息软件(如地理信息系统)的核心组件使用。它还可以作为一个通用库,提供计算几何学中的算法。

  • WGS-84坐标系 : 国际标准,真实坐标。全球定位系统直接获取的就是WGS-84坐标。
  • GCJ-02坐标系 : 国家加密,偏移坐标。通过一个特殊的非线性算法,对WGS-84的真实坐标进行了加密偏移,偏移量从几百米到上千米不等。
  • WKT(Well-Known Text):是一种用于表示地理空间几何对象的文本标记语言,在 GIS(地理信息系统)、数据库(如 PostGIS、MySQL、SQL Server)和各类地图开发中广泛使用。点 POINT(116.397428 39.909204)
  • Geohash :是一种地理编码系统,将二维的经纬度坐标转换为一维的字符串编码。它是一种分级递推的空间数据结构,广泛应用于地理信息系统、数据库索引和位置服务中。

maven依赖

<dependency>
    <groupId>org.locationtech.jts</groupId>
    <artifactId>jts-core</artifactId>
    <version>1.18.2</version>
</dependency>

代码

坐标距离计算工具类

package com.imooc.sourcecode2.jts;

import lombok.extern.slf4j.Slf4j;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.io.ParseException;
import org.locationtech.jts.io.WKTReader;

/**
 * 坐标距离计算工具类
 * 提供在不同坐标系下计算两点之间距离的方法,以及点到多边形的距离计算
 * 支持高德坐标系(GCJ-02)和WGS84坐标系
 */
@Slf4j
public class CoordDistanceUtils {

    /**
     * 地球平均半径(米)
     */
    private static final double EARTH_RADIUS = 6371000;
    
    /**
     * 几何图形工厂,线程安全,可以共享使用
     */
    private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory();
    
    /**
     * WKTReader线程本地变量,确保线程安全
     */
    private static final ThreadLocal<WKTReader> WKT_READER_THREAD_LOCAL = 
        ThreadLocal.withInitial(() -> new WKTReader(GEOMETRY_FACTORY));

    /**
     * 计算两个WGS84坐标点之间的距离(米)
     * 使用Haversine公式计算球面距离
     *
     * @param lat1 第一个点的纬度
     * @param lng1 第一个点的经度
     * @param lat2 第二个点的纬度
     * @param lng2 第二个点的经度
     * @return 两点之间的距离,单位:米
     */
    public static double calculateDistanceInWgs84(double lat1, double lng1, double lat2, double lng2) {
        // 转换为弧度
        double radLat1 = Math.toRadians(lat1);
        double radLng1 = Math.toRadians(lng1);
        double radLat2 = Math.toRadians(lat2);
        double radLng2 = Math.toRadians(lng2);
        
        // 计算经纬度差值
        double deltaLat = radLat2 - radLat1;
        double deltaLng = radLng2 - radLng1;
        
        // Haversine公式
        double a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
                   Math.cos(radLat1) * Math.cos(radLat2) *
                   Math.sin(deltaLng / 2) * Math.sin(deltaLng / 2);
        double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
        
        // 计算距离
        return EARTH_RADIUS * c;
    }
    
    /**
     * 计算两个高德坐标系(GCJ-02)点之间的距离(米)
     * 先将高德坐标转换为WGS84坐标,再计算距离
     *
     * @param lat1 第一个点的纬度(高德坐标系)
     * @param lng1 第一个点的经度(高德坐标系)
     * @param lat2 第二个点的纬度(高德坐标系)
     * @param lng2 第二个点的经度(高德坐标系)
     * @return 两点之间的距离,单位:米
     */
    public static double calculateDistanceInGcj02(double lat1, double lng1, double lat2, double lng2) {
        // 将高德坐标转换为WGS84坐标
        double[] wgsCoord1 = CoordTransUtils.gcj02ToWgs84(lat1, lng1);
        double[] wgsCoord2 = CoordTransUtils.gcj02ToWgs84(lat2, lng2);
        
        // 计算WGS84坐标下的距离
        return calculateDistanceInWgs84(wgsCoord1[0], wgsCoord1[1], wgsCoord2[0], wgsCoord2[1]);
    }
    
    /**
     * 计算点到WKT多边形的最短距离(米)
     * 如果点在多边形内部,返回0
     *
     * @param lat 点的纬度(WGS84坐标系)
     * @param lng 点的经度(WGS84坐标系)
     * @param wkt 多边形的WKT表示(WGS84坐标系)
     * @return 点到多边形的最短距离,单位:米
     */
    public static double calculateDistanceToPolygonInWgs84(double lat, double lng, String wkt) {
        try {
            // 创建点
            Point point = GEOMETRY_FACTORY.createPoint(new Coordinate(lng, lat));
            
            // 解析多边形
            WKTReader reader = WKT_READER_THREAD_LOCAL.get();
            Geometry polygon = reader.read(wkt);
            
            // 计算距离(JTS返回的是度数单位的距离)
            double degreeDistance = polygon.distance(point);
            
            // 如果点在多边形内部,距离为0
            if (degreeDistance == 0) {
                return 0;
            }
            
            // 将度数距离转换为米
            // 这是一个近似计算,在小距离内较为准确
            // 纬度1度约等于111km,经度1度随纬度变化
            double latDegreeToMeter = 111000; // 纬度1度约等于111km
            double lngDegreeToMeter = 111000 * Math.cos(Math.toRadians(lat)); // 经度1度随纬度变化
            
            // 找到多边形上距离点最近的点
            Coordinate nearestPoint = polygon.getFactory().createPoint(polygon.getCoordinate()).getCoordinate();
            for (Coordinate coord : polygon.getCoordinates()) {
                Point tempPoint = GEOMETRY_FACTORY.createPoint(coord);
                if (point.distance(tempPoint) < point.distance(GEOMETRY_FACTORY.createPoint(nearestPoint))) {
                    nearestPoint = coord;
                }
            }
            
            // 计算点到最近点的距离
            return calculateDistanceInWgs84(lat, lng, nearestPoint.y, nearestPoint.x);
        } catch (ParseException e) {
            log.error("解析WKT字符串失败: {}", wkt, e);
            throw new IllegalArgumentException("无效的WKT格式: " + wkt, e);
        }
    }
    
    /**
     * 计算点到WKT多边形的最短距离(米)
     * 如果点在多边形内部,返回0
     * 注意:输入的坐标和WKT均为高德坐标系(GCJ-02)
     *
     * @param lat 点的纬度(高德坐标系)
     * @param lng 点的经度(高德坐标系)
     * @param wkt 多边形的WKT表示(高德坐标系)
     * @return 点到多边形的最短距离,单位:米
     */
    public static double calculateDistanceToPolygonInGcj02(double lat, double lng, String wkt) {
        // 将高德坐标转换为WGS84坐标
        double[] wgsCoord = CoordTransUtils.gcj02ToWgs84(lat, lng);
        
        // 将高德坐标系WKT转换为WGS84坐标系WKT
        String wgs84Wkt = CoordTransUtils.gcjWktToWgsWkt(wkt);
        
        // 计算WGS84坐标下的距离
        return calculateDistanceToPolygonInWgs84(wgsCoord[0], wgsCoord[1], wgs84Wkt);
    }
    
    /**
     * 判断两个点之间的距离是否小于指定阈值
     *
     * @param lat1 第一个点的纬度(高德坐标系)
     * @param lng1 第一个点的经度(高德坐标系)
     * @param lat2 第二个点的纬度(高德坐标系)
     * @param lng2 第二个点的经度(高德坐标系)
     * @param thresholdMeters 阈值距离,单位:米
     * @return 如果距离小于阈值,返回true;否则返回false
     */
    public static boolean isWithinDistance(double lat1, double lng1, double lat2, double lng2, double thresholdMeters) {
        double distance = calculateDistanceInGcj02(lat1, lng1, lat2, lng2);
        return distance <= thresholdMeters;
    }
    
    /**
     * 判断点是否在多边形的指定距离范围内
     * 如果点在多边形内部或距离小于指定阈值,返回true
     *
     * @param lat 点的纬度(高德坐标系)
     * @param lng 点的经度(高德坐标系)
     * @param wkt 多边形的WKT表示(高德坐标系)
     * @param thresholdMeters 阈值距离,单位:米
     * @return 如果点在多边形内部或距离小于阈值,返回true;否则返回false
     */
    public static boolean isWithinDistanceOfPolygon(double lat, double lng, String wkt, double thresholdMeters) {
        double distance = calculateDistanceToPolygonInGcj02(lat, lng, wkt);
        return distance <= thresholdMeters;
    }
} 
 

坐标系转换工具类

package com.imooc.sourcecode2.jts;

import lombok.extern.slf4j.Slf4j;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.util.GeometryEditor;
import org.locationtech.jts.io.ParseException;
import org.locationtech.jts.io.WKTReader;
import org.locationtech.jts.io.WKTWriter;

/**
 * 坐标系转换工具类
 * 提供在高德坐标系(GCJ-02)和WGS84坐标系之间的转换功能
 * GCJ-02: 中国国测局坐标系,高德、腾讯等国内地图使用
 * WGS84: 国际标准坐标系,GPS和Google Earth使用
 */
@Slf4j
public class CoordTransUtils {

    /**
     * 地球半径,单位:米
     */
    private static final double EARTH_RADIUS = 6378245.0;
    
    /**
     * 坐标系偏移参数
     */
    private static final double EE = 0.00669342162296594323;
    
    /**
     * 几何图形工厂,线程安全,可以共享使用
     */
    private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory();
    
    /**
     * WKTReader线程本地变量,确保线程安全
     */
    private static final ThreadLocal<WKTReader> WKT_READER_THREAD_LOCAL = 
        ThreadLocal.withInitial(() -> new WKTReader(GEOMETRY_FACTORY));
    
    /**
     * WKTWriter线程本地变量,确保线程安全
     */
    private static final ThreadLocal<WKTWriter> WKT_WRITER_THREAD_LOCAL = 
        ThreadLocal.withInitial(WKTWriter::new);

    /**
     * 将高德坐标系(GCJ-02)转换为WGS84坐标系
     *
     * @param lat 高德坐标系的纬度
     * @param lng 高德坐标系的经度
     * @return WGS84坐标系的坐标,[纬度, 经度]
     */
    public static double[] gcj02ToWgs84(double lat, double lng) {
        if (isOutOfChina(lat, lng)) {
            return new double[]{lat, lng};
        }
        
        double dLat = transformLat(lng - 105.0, lat - 35.0);
        double dLng = transformLng(lng - 105.0, lat - 35.0);
        
        double radLat = lat / 180.0 * Math.PI;
        double magic = Math.sin(radLat);
        magic = 1 - EE * magic * magic;
        
        double sqrtMagic = Math.sqrt(magic);
        dLat = (dLat * 180.0) / ((EARTH_RADIUS * (1 - EE)) / (magic * sqrtMagic) * Math.PI);
        dLng = (dLng * 180.0) / (EARTH_RADIUS / sqrtMagic * Math.cos(radLat) * Math.PI);
        
        double wgsLat = lat - dLat;
        double wgsLng = lng - dLng;
        
        return new double[]{wgsLat, wgsLng};
    }

    /**
     * 将WGS84坐标系转换为高德坐标系(GCJ-02)
     *
     * @param lat WGS84坐标系的纬度
     * @param lng WGS84坐标系的经度
     * @return 高德坐标系的坐标,[纬度, 经度]
     */
    public static double[] wgs84ToGcj02(double lat, double lng) {
        if (isOutOfChina(lat, lng)) {
            return new double[]{lat, lng};
        }
        
        double dLat = transformLat(lng - 105.0, lat - 35.0);
        double dLng = transformLng(lng - 105.0, lat - 35.0);
        
        double radLat = lat / 180.0 * Math.PI;
        double magic = Math.sin(radLat);
        magic = 1 - EE * magic * magic;
        
        double sqrtMagic = Math.sqrt(magic);
        dLat = (dLat * 180.0) / ((EARTH_RADIUS * (1 - EE)) / (magic * sqrtMagic) * Math.PI);
        dLng = (dLng * 180.0) / (EARTH_RADIUS / sqrtMagic * Math.cos(radLat) * Math.PI);
        
        double gcjLat = lat + dLat;
        double gcjLng = lng + dLng;
        
        return new double[]{gcjLat, gcjLng};
    }

    /**
     * 将高德坐标系(GCJ-02)的WKT字符串转换为WGS84坐标系的WKT字符串
     *
     * @param gcjWkt 高德坐标系的WKT字符串
     * @return WGS84坐标系的WKT字符串
     */
    public static String gcjWktToWgsWkt(String gcjWkt) {
        try {
            // 解析WKT字符串为几何对象
            WKTReader reader = WKT_READER_THREAD_LOCAL.get();
            Geometry geometry = reader.read(gcjWkt);
            
            // 转换几何对象中的所有坐标
            Geometry transformedGeometry = transformGeometry(geometry, true);
            
            // 将转换后的几何对象转换回WKT字符串
            WKTWriter writer = WKT_WRITER_THREAD_LOCAL.get();
            return writer.write(transformedGeometry);
        } catch (ParseException e) {
            log.error("解析WKT字符串失败: {}", gcjWkt, e);
            throw new IllegalArgumentException("无效的WKT格式: " + gcjWkt, e);
        }
    }

    /**
     * 将WGS84坐标系的WKT字符串转换为高德坐标系(GCJ-02)的WKT字符串
     *
     * @param wgsWkt WGS84坐标系的WKT字符串
     * @return 高德坐标系的WKT字符串
     */
    public static String wgsWktToGcjWkt(String wgsWkt) {
        try {
            // 解析WKT字符串为几何对象
            WKTReader reader = WKT_READER_THREAD_LOCAL.get();
            Geometry geometry = reader.read(wgsWkt);
            
            // 转换几何对象中的所有坐标
            Geometry transformedGeometry = transformGeometry(geometry, false);
            
            // 将转换后的几何对象转换回WKT字符串
            WKTWriter writer = WKT_WRITER_THREAD_LOCAL.get();
            return writer.write(transformedGeometry);
        } catch (ParseException e) {
            log.error("解析WKT字符串失败: {}", wgsWkt, e);
            throw new IllegalArgumentException("无效的WKT格式: " + wgsWkt, e);
        }
    }

    /**
     * 将高德坐标系(GCJ-02)的几何对象转换为WGS84坐标系的几何对象
     *
     * @param gcjGeometry 高德坐标系的几何对象
     * @return WGS84坐标系的几何对象
     */
    public static Geometry gcjGeometryToWgsGeometry(Geometry gcjGeometry) {
        return transformGeometry(gcjGeometry, true);
    }

    /**
     * 将WGS84坐标系的几何对象转换为高德坐标系(GCJ-02)的几何对象
     *
     * @param wgsGeometry WGS84坐标系的几何对象
     * @return 高德坐标系的几何对象
     */
    public static Geometry wgsGeometryToGcjGeometry(Geometry wgsGeometry) {
        return transformGeometry(wgsGeometry, false);
    }

    /**
     * 将高德坐标系(GCJ-02)的WKT字符串解析并转换为WGS84坐标系的几何对象
     *
     * @param gcjWkt 高德坐标系的WKT字符串
     * @return WGS84坐标系的几何对象
     * @throws IllegalArgumentException 当WKT格式无效时抛出
     */
    public static Geometry gcjWktToWgsGeometry(String gcjWkt) {
        try {
            // 解析WKT字符串为几何对象
            WKTReader reader = WKT_READER_THREAD_LOCAL.get();
            Geometry geometry = reader.read(gcjWkt);
            
            // 转换几何对象中的所有坐标
            return transformGeometry(geometry, true);
        } catch (ParseException e) {
            log.error("解析WKT字符串失败: {}", gcjWkt, e);
            throw new IllegalArgumentException("无效的WKT格式: " + gcjWkt, e);
        }
    }

    /**
     * 将WGS84坐标系的WKT字符串解析并转换为高德坐标系(GCJ-02)的几何对象
     *
     * @param wgsWkt WGS84坐标系的WKT字符串
     * @return 高德坐标系的几何对象
     * @throws IllegalArgumentException 当WKT格式无效时抛出
     */
    public static Geometry wgsWktToGcjGeometry(String wgsWkt) {
        try {
            // 解析WKT字符串为几何对象
            WKTReader reader = WKT_READER_THREAD_LOCAL.get();
            Geometry geometry = reader.read(wgsWkt);
            
            // 转换几何对象中的所有坐标
            return transformGeometry(geometry, false);
        } catch (ParseException e) {
            log.error("解析WKT字符串失败: {}", wgsWkt, e);
            throw new IllegalArgumentException("无效的WKT格式: " + wgsWkt, e);
        }
    }

    /**
     * 转换几何对象中的所有坐标
     * 使用GeometryEditor简化实现
     *
     * @param geometry 原始几何对象
     * @param isGcjToWgs 是否从GCJ-02转换到WGS84,true表示GCJ-02到WGS84,false表示WGS84到GCJ-02
     * @return 转换后的几何对象
     */
    private static Geometry transformGeometry(Geometry geometry, boolean isGcjToWgs) {
        // 使用GeometryEditor修改几何对象的坐标
        GeometryEditor editor = new GeometryEditor(GEOMETRY_FACTORY);
        
        // 使用匿名内部类实现GeometryEditor.CoordinateOperation接口
        return editor.edit(geometry, new GeometryEditor.CoordinateOperation() {
            @Override
            public Coordinate[] edit(Coordinate[] coordinates, Geometry geometry) {
                Coordinate[] result = new Coordinate[coordinates.length];
                
                for (int i = 0; i < coordinates.length; i++) {
                    Coordinate coord = coordinates[i];
                    double[] transformed;
                    
                    if (isGcjToWgs) {
                        transformed = gcj02ToWgs84(coord.y, coord.x);
                    } else {
                        transformed = wgs84ToGcj02(coord.y, coord.x);
                    }
                    
                    result[i] = new Coordinate(transformed[1], transformed[0], coord.z);
                }
                
                return result;
            }
        });
    }

    /**
     * 判断坐标是否在中国范围外
     * 
     * @param lat 纬度
     * @param lng 经度
     * @return 是否在中国范围外
     */
    private static boolean isOutOfChina(double lat, double lng) {
        if (lng < 72.004 || lng > 137.8347) {
            return true;
        }
        if (lat < 0.8293 || lat > 55.8271) {
            return true;
        }
        return false;
    }

    /**
     * 转换纬度
     */
    private static double transformLat(double x, double y) {
        double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));
        ret += (20.0 * Math.sin(6.0 * x * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0 / 3.0;
        ret += (20.0 * Math.sin(y * Math.PI) + 40.0 * Math.sin(y / 3.0 * Math.PI)) * 2.0 / 3.0;
        ret += (160.0 * Math.sin(y / 12.0 * Math.PI) + 320 * Math.sin(y * Math.PI / 30.0)) * 2.0 / 3.0;
        return ret;
    }

    /**
     * 转换经度
     */
    private static double transformLng(double x, double y) {
        double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
        ret += (20.0 * Math.sin(6.0 * x * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0 / 3.0;
        ret += (20.0 * Math.sin(x * Math.PI) + 40.0 * Math.sin(x / 3.0 * Math.PI)) * 2.0 / 3.0;
        ret += (150.0 * Math.sin(x / 12.0 * Math.PI) + 300.0 * Math.sin(x / 30.0 * Math.PI)) * 2.0 / 3.0;
        return ret;
    }
}

GeoHash工具类,用于处理地理空间数据的编码和多边形计算

package com.imooc.sourcecode2.jts;

import ch.hsr.geohash.BoundingBox;
import ch.hsr.geohash.GeoHash;
import lombok.extern.slf4j.Slf4j;
import org.locationtech.jts.geom.*;
import org.locationtech.jts.io.ParseException;
import org.locationtech.jts.io.WKTReader;

import java.util.*;

/**
 * GeoHash工具类,用于处理地理空间数据的编码和多边形计算
 * Geohash精度说明(1-12):
 * 精度 | 字符长度 | 覆盖范围      | 典型用途
 * -----------------------------------------------
 * 1    | 1        | ~5000 km     | 大洲/国家级别
 * 2    | 2        | ~1250 km     | 国家/大区域
 * 3    | 3        | ~156 km      | 省级/大城市
 * 4    | 4        | ~39 km       | 城市级
 * 5    | 5        | ~5 km        | 区县/城镇
 * 6    | 6        | ~1.2 km      | 街道/社区
 * 7    | 7        | ~150 m       | 地标建筑
 * 8    | 8        | ~19 m        | 小型区域
 * 9    | 9        | ~3.7 m       | 建筑物轮廓
 * 10   | 10       | ~0.5 m       | 停车位/房间
 * 11   | 11       | ~6 cm        | 高精度测绘
 * 12   | 12       | ~3.7 cm      | 特殊定位需求
 * 注:覆盖范围为纬度方向近似值,精度随纬度变化略有差异。
 * 
 * 注意:本工具类默认输入的坐标为高德坐标系(GCJ-02),内部会自动转换为WGS84坐标系进行计算
 */
@Slf4j
public class GeoHashUtils {
    
    /**
     * 几何图形工厂,线程安全,可以共享使用
     */
    private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory();
    
    /**
     * WKTReader线程本地变量,确保线程安全
     */
    private static final ThreadLocal<WKTReader> WKT_READER_THREAD_LOCAL = 
        ThreadLocal.withInitial(() -> new WKTReader(GEOMETRY_FACTORY));
    
    /**
     * 纬度极值:正常计算GeoHash的纬度上限
     */
    private static final double MAX_EXTREME_LATITUDE = 85.0;
    
    /**
     * 纬度极值:正常计算GeoHash的纬度下限
     */
    private static final double MIN_EXTREME_LATITUDE = -85.0;
    
    /**
     * GeoHash计算时行的最大安全限制,防止死循环
     */
    private static final int MAX_GEOHASH_ROWS = 1000;
    
    /**
     * GeoHash计算时列的最大安全限制,防止死循环
     */
    private static final int MAX_GEOHASH_COLS = 1000;

    /**
     * 计算WKT多边形覆盖的GeoHash列表
     * 注意:输入的WKT为高德坐标系(GCJ-02),内部会自动转换为WGS84坐标系进行计算
     *
     * @param wkt       WKT格式的多边形字符串(高德坐标系)
     * @param precision GeoHash精度,取值范围1-12,数字越大精度越高
     * @return 与多边形相交的GeoHash集合
     * @throws IllegalArgumentException 当WKT格式无效时抛出
     */
    public static Set<String> getIntersectingGeoHashes(String wkt, int precision) {
        if (wkt == null || wkt.isEmpty()) {
            return Collections.emptySet();
        }
        validatePrecision(precision);
        
        try {
            // 直接获取WGS84坐标系的几何对象,避免不必要的WKT转换
            Geometry geometry = CoordTransUtils.gcjWktToWgsGeometry(wkt);
            return getIntersectingGeoHashesForGeometry(geometry, precision);
        } catch (IllegalArgumentException e) {
            log.error("Invalid WKT format: {}", wkt, e);
            throw e;
        }
    }

    /**
     * 判断指定坐标是否在WKT多边形内部
     * 注意:输入的坐标和WKT均为高德坐标系(GCJ-02),内部会自动转换为WGS84坐标系进行计算
     *
     * @param lat 纬度(高德坐标系)
     * @param lon 经度(高德坐标系)
     * @param wkt WKT格式的多边形字符串(高德坐标系)
     * @return true表示点在多边形内,false表示点在多边形外
     * @throws IllegalArgumentException 当WKT格式无效时抛出
     */
    public static boolean isPointInPolygon(double lat, double lon, String wkt) {
        try {
            // 将高德坐标系转换为WGS84坐标系
            double[] wgsCoord = CoordTransUtils.gcj02ToWgs84(lat, lon);

            // 直接获取WGS84坐标系的几何对象,避免不必要的WKT转换
            Geometry geometry = CoordTransUtils.gcjWktToWgsGeometry(wkt);
            Point point = GEOMETRY_FACTORY.createPoint(new Coordinate(wgsCoord[1], wgsCoord[0]));
            return geometry.covers(point);
        } catch (IllegalArgumentException e) {
            log.error("Invalid WKT format: {}", wkt, e);
            throw e;
        }
    }

    /**
     * 判断指定坐标是否在WKT多边形内部(不进行坐标系转换)
     * 注意:输入的坐标和WKT必须是同一个坐标系
     *
     * @param lat 纬度
     * @param lon 经度
     * @param wkt WKT格式的多边形字符串
     * @return true表示点在多边形内,false表示点在多边形外
     * @throws IllegalArgumentException 当WKT格式无效时抛出
     */
    public static boolean isPointInPolygonWithoutTransform(double lat, double lon, String wkt) {
        try {
            // 直接解析WKT字符串为几何对象,不进行坐标系转换
            WKTReader reader = WKT_READER_THREAD_LOCAL.get();
            Geometry geometry = reader.read(wkt);
            
            // 创建点对象,注意JTS中坐标顺序是(经度, 纬度)
            Point point = GEOMETRY_FACTORY.createPoint(new Coordinate(lon, lat));
            
            // 判断点是否在多边形内
            return geometry.covers(point);
        } catch (ParseException e) {
            log.error("Invalid WKT format: {}", wkt, e);
            throw new IllegalArgumentException("Invalid WKT format: " + wkt, e);
        }
    }

    /**
     * 判断指定WGS84坐标系的坐标是否在WGS84坐标系的WKT多边形内部
     *
     * @param lat 纬度(WGS84坐标系)
     * @param lon 经度(WGS84坐标系)
     * @param wkt WKT格式的多边形字符串(WGS84坐标系)
     * @return true表示点在多边形内,false表示点在多边形外
     * @throws IllegalArgumentException 当WKT格式无效时抛出
     */
    public static boolean isWgs84PointInWgs84Polygon(double lat, double lon, String wkt) {
        return isPointInPolygonWithoutTransform(lat, lon, wkt);
    }

    /**
     * 判断指定高德坐标系(GCJ-02)的坐标是否在高德坐标系的WKT多边形内部
     *
     * @param lat 纬度(高德坐标系)
     * @param lon 经度(高德坐标系)
     * @param wkt WKT格式的多边形字符串(高德坐标系)
     * @return true表示点在多边形内,false表示点在多边形外
     * @throws IllegalArgumentException 当WKT格式无效时抛出
     */
    public static boolean isGcj02PointInGcj02Polygon(double lat, double lon, String wkt) {
        return isPointInPolygonWithoutTransform(lat, lon, wkt);
    }

    /**
     * 计算指定坐标的GeoHash值
     * 注意:输入的坐标为高德坐标系(GCJ-02),内部会自动转换为WGS84坐标系进行计算
     *
     * @param lat       纬度(高德坐标系)
     * @param lon       经度(高德坐标系)
     * @param precision GeoHash精度,取值范围1-12
     * @return GeoHash字符串
     */
    public static String calculateGeoHash(double lat, double lon, int precision) {
        // 将高德坐标系转换为WGS84坐标系
        double[] wgsCoord = CoordTransUtils.gcj02ToWgs84(lat, lon);
        return GeoHash.withCharacterPrecision(wgsCoord[0], wgsCoord[1], precision).toBase32();
    }

    /**
     * 将GeoHash集合转换为MultiPolygon,保持每个GeoHash的独立多边形形式
     *
     * @param geoHashes GeoHash字符串集合
     * @return MultiPolygon 包含所有GeoHash对应的独立多边形,不进行合并
     */
    public static MultiPolygon getPolygonsFromHashes(Set<String> geoHashes) {
        if (geoHashes == null || geoHashes.isEmpty()) {
            return GEOMETRY_FACTORY.createMultiPolygon(null);
        }

        Polygon[] polygons = new Polygon[geoHashes.size()];
        int index = 0;

        for (String hash : geoHashes) {
            BoundingBox box = GeoHash.fromGeohashString(hash).getBoundingBox();
            polygons[index++] = createGeoHashPolygon(box);
        }

        return GEOMETRY_FACTORY.createMultiPolygon(polygons);
    }

    /**
     * 将GeoHash列表转换为MultiPolygon对象
     *
     * @param geoHashes GeoHash字符串集合
     * @return 由GeoHash区域组成的MultiPolygon对象
     */
    public static MultiPolygon convertHashesToMultiPolygon(Set<String> geoHashes) {
        List<Polygon> polygons = new ArrayList<>(geoHashes.size());

        for (String geoHash : geoHashes) {
            BoundingBox box = GeoHash.fromGeohashString(geoHash).getBoundingBox();
            polygons.add(createGeoHashPolygon(box));
        }

        // 合并所有多边形
        Geometry union = null;
        for (Polygon polygon : polygons) {
            if (union == null) {
                union = polygon;
                continue;
            }
            union = union.union(polygon);
        }

        // 处理合并结果
        if (union instanceof MultiPolygon) {
            return (MultiPolygon) union;
        }
        if (union instanceof Polygon) {
            return GEOMETRY_FACTORY.createMultiPolygon(new Polygon[]{(Polygon) union});
        }
        return GEOMETRY_FACTORY.createMultiPolygon(null);
    }

    private static Set<String> getIntersectingGeoHashesForGeometry(Geometry geometry, int precision) {
        Set<String> geoHashes = new HashSet<>();

        if (geometry instanceof MultiPolygon) {
            MultiPolygon multiPolygon = (MultiPolygon) geometry;
            for (int i = 0; i < multiPolygon.getNumGeometries(); i++) {
                Polygon polygon = (Polygon) multiPolygon.getGeometryN(i);
                geoHashes.addAll(calculateGeoHashesForPolygon(polygon, precision));
            }
        } else if (geometry instanceof Polygon) {
            geoHashes.addAll(calculateGeoHashesForPolygon((Polygon) geometry, precision));
        }

        return geoHashes;
    }

    /**
     * 计算多边形覆盖的GeoHash列表
     *
     * @param polygon   目标多边形
     * @param precision GeoHash精度
     * @return 与多边形相交的GeoHash集合
     */
    private static Set<String> calculateGeoHashesForPolygon(Polygon polygon, int precision) {
        Set<String> geoHashes = new LinkedHashSet<>();

        // 获取多边形的边界框
        Envelope envelope = polygon.getEnvelopeInternal();

        // 创建完整覆盖边界框的最小GeoHash集合
        Set<String> coveringHashes = getCoveringGeoHashes(
                envelope.getMinY(), envelope.getMinX(),
                envelope.getMaxY(), envelope.getMaxX(),
                precision);

        // 验证每个GeoHash是否与多边形相交
        for (String hash : coveringHashes) {
            if (intersectsWithPolygon(hash, polygon)) {
                geoHashes.add(hash);
            }
        }

        return geoHashes;
    }

    /**
     * 判断GeoHash区域是否与多边形相交
     *
     * @param geoHash GeoHash字符串
     * @param polygon 目标多边形
     * @return 是否相交
     */
    private static boolean intersectsWithPolygon(String geoHash, Polygon polygon) {
        BoundingBox box = GeoHash.fromGeohashString(geoHash).getBoundingBox();
        Polygon geoHashPoly = createGeoHashPolygon(box);
        return polygon.intersects(geoHashPoly);
    }

    /**
     * 创建表示GeoHash区域的多边形
     *
     * @param box GeoHash的边界框
     * @return 表示GeoHash区域的多边形
     */
    private static Polygon createGeoHashPolygon(BoundingBox box) {
        Coordinate[] coords = new Coordinate[5];
        coords[0] = new Coordinate(box.getWestLongitude(), box.getSouthLatitude());
        coords[1] = new Coordinate(box.getEastLongitude(), box.getSouthLatitude());
        coords[2] = new Coordinate(box.getEastLongitude(), box.getNorthLatitude());
        coords[3] = new Coordinate(box.getWestLongitude(), box.getNorthLatitude());
        // 闭合多边形
        coords[4] = coords[0];  
        return GEOMETRY_FACTORY.createPolygon(coords);
    }

    private static void validatePrecision(int precision) {
        if (precision < 1 || precision > 12) {
            throw new IllegalArgumentException("Precision must be between 1 and 12");
        }
    }

    /**
     * 获取覆盖指定边界框的GeoHash集合
     * 添加安全措施防止在极端纬度区域出现无限循环
     *
     * @param minLat    最小纬度
     * @param minLon    最小经度
     * @param maxLat    最大纬度
     * @param maxLon    最大经度
     * @param precision GeoHash精度
     * @return 覆盖边界框的GeoHash集合
     * @throws IllegalArgumentException 当坐标在极值区域或跨越国际日期变更线时抛出
     */
    private static Set<String> getCoveringGeoHashes(double minLat, double minLon,
                                                 double maxLat, double maxLon,
                                                 int precision) {
        minLat = Math.max(minLat, -90.0);
        maxLat = Math.min(maxLat, 90.0);
        minLon = normalizeLongitude(minLon);
        maxLon = normalizeLongitude(maxLon);
        
        // 处理极端纬度区域(接近南北极)- 直接抛出异常
        if (minLat > MAX_EXTREME_LATITUDE || maxLat < MIN_EXTREME_LATITUDE) {
            throw new IllegalArgumentException(String.format("坐标超出可计算GeoHash的纬度范围(%f, %f), 请使用纬度范围在%f到%f之间的坐标",
                    minLat, maxLat, MIN_EXTREME_LATITUDE, MAX_EXTREME_LATITUDE));
        }
        
        // 处理跨180度经线的特殊情况 - 在中国区域不会出现,直接抛出异常
        if (minLon > maxLon) {
            throw new IllegalArgumentException(String.format("经度范围异常: 最小经度(%f)大于最大经度(%f), 这在中国区域是不可能的情况,请检查输入坐标", 
                    minLon, maxLon));
        }
        
        return getCoveringGeoHashesNoCrossing(minLat, minLon, maxLat, maxLon, precision);
    }
    
    /**
     * 处理不跨越日期线的普通区域的GeoHash覆盖
     * 
     * @param minLat    最小纬度
     * @param minLon    最小经度
     * @param maxLat    最大纬度
     * @param maxLon    最大经度
     * @param precision GeoHash精度
     * @return 覆盖边界框的GeoHash集合
     */
    private static Set<String> getCoveringGeoHashesNoCrossing(double minLat, double minLon,
                                                          double maxLat, double maxLon,
                                                          int precision) {
        // 参数校验和规范化
        minLat = Math.max(minLat, -90.0);
        maxLat = Math.min(maxLat, 90.0);
        minLon = normalizeLongitude(minLon);
        maxLon = normalizeLongitude(maxLon);
        
        // 极端纬度检查(应该已在上层方法中过滤,这里作为额外保护)
        if (minLat > MAX_EXTREME_LATITUDE || maxLat < MIN_EXTREME_LATITUDE) {
            throw new IllegalArgumentException(
                String.format("坐标超出可计算GeoHash的纬度范围(%f, %f)", minLat, maxLat));
        }
        
        // 使用LinkedHashSet保证遍历顺序
        Set<String> hashes = new LinkedHashSet<>();
        
        // 计算边界的GeoHash
        GeoHash bottomLeft = GeoHash.withCharacterPrecision(minLat, minLon, precision);
        GeoHash topRight = GeoHash.withCharacterPrecision(maxLat, maxLon, precision);
        
        // 从左下角开始添加所有可能的GeoHash
        GeoHash current = bottomLeft;
        
        // 行计数
        int rowCount = 0;
        
        // 按行遍历,从南向北
        while (current != null && 
               current.getBoundingBox().getNorthLatitude() <= topRight.getBoundingBox().getNorthLatitude() &&
               rowCount < MAX_GEOHASH_ROWS) {
               
            GeoHash rowHash = current;
            
            // 列计数
            int colCount = 0;
            
            // 按列遍历,从西向东
            while (rowHash != null && 
                   rowHash.getBoundingBox().getEastLongitude() <= topRight.getBoundingBox().getEastLongitude() &&
                   colCount < MAX_GEOHASH_COLS) {
                   
                hashes.add(rowHash.toBase32());
                
                // 获取东侧相邻的GeoHash
                rowHash = rowHash.getEasternNeighbour();
                colCount++;
                
                // 处理跨经线问题
                if (rowHash != null && rowHash.getBoundingBox().getWestLongitude() >
                    rowHash.getBoundingBox().getEastLongitude()) {
                    break;
                }
            }
            
            // 检查列是否达到上限,并立即记录警告日志
            if (colCount >= MAX_GEOHASH_COLS) {
                log.warn("GeoHash计算在第{}行达到列数限制: 边界({}, {}, {}, {}), 精度: {}, 当前行处理了{}个GeoHash",
                        rowCount + 1, minLat, minLon, maxLat, maxLon, precision, colCount);
            }
            
            // 获取北侧相邻的GeoHash
            current = current.getNorthernNeighbour();
            rowCount++;
        }
        
        // 如果达到行数限制,记录警告日志
        if (rowCount >= MAX_GEOHASH_ROWS) {
            log.warn("GeoHash计算达到行数限制: 边界({}, {}, {}, {}), 精度: {}, 获取到 {} 个GeoHash",
                    minLat, minLon, maxLat, maxLon, precision, hashes.size());
        }
        
        return hashes;
    }

    private static double normalizeLongitude(double lon) {
        while (lon > 180.0) {
            lon -= 360.0;
        }
        while (lon < -180.0) {
            lon += 360.0;
        }
        return lon;
    }

}

参考

JTS 介绍

posted @ 2026-03-31 08:30  strongmore  阅读(9)  评论(0)    收藏  举报