前进之路 发表于 2023-7-28 16:21:25

PostgreSQL+GeoHash地图点位聚合

PG数据库安装扩展

需要用到pg数据库的空间扩展postgis,在进行操作之前需要在数据库中安装扩展。
CREATE EXTENSION postgis;
CREATE EXTENSION postgis_topology;
CREATE EXTENSION postgis_geohash;GeoHash

GeoHash是一种地址编码方法。他能够把二维的空间经纬度数据编码成一个字符串。具体原理这里不再详细说明,GeoHash算法大体上分为三步:

[*]将经纬度变成二进制
[*]将经纬度的二进制合并
[*]通过Base32对合并后的二进制进行编码
Geohash比直接用经纬度的高效很多,而且使用者可以发布地址编码,既能表明自己位于北海公园附近,又不至于暴露自己的精确坐标,有助于隐私保护。

[*]GeoHash用一个字符串表示经度和纬度两个坐标。在数据库中可以实现在一列上应用索引(某些情况下无法在两列上同时应用索引)
[*]GeoHash表示的并不是一个点,而是一个矩形区域
[*]GeoHash编码的前缀可以表示更大的区域。例如wx4g0ec1,它的前缀wx4g0e表示包含编码wx4g0ec1在内的更大范围。 这个特性可以用于附近地点搜索
[*]编码越长,表示的范围越小,位置也越精确。因此我们就可以通过比较GeoHash匹配的位数来判断两个点之间的大概距离
建表

在创建数据库表时,表中除了经纬度字段以外,再创建两个字段:
① 经纬度对应的Geometry字段(类型:geometry)
② 经纬度对应的geoHash值字段(类型:varchar)
如:alter table 表名 add 字段名 geometry(point, 4326); // 创建geometry字段
alter table 表名 add 字段名 varchar; // 创建geoHash字段
JPA中定义

@Type(type="jts_geometry")
@Column(name="geometry",columnDefinition = "geometry(Point,4326)")
@JsonIgnore
private Geometry geometry; // 实体类的Geometry字段根据经纬度计算 geometry 和 geoHash

Java生成geometry和geoHash

geometry字段 和 geoHash字段均可以在java代码中根据经纬度生成。
根据经纬度生成geometry
使用org.locationtech.jts.io包下的WKTReader类,可以根据经纬度生成Geometry对象。
String wkt = "POINT("+longitude+" "+latitude+")"; // longitude 经度,latitude纬度
WKTReader wktReader = new WKTReader();
Geometry geometry = wktReader.read(wkt); // Geometry对象
if(geometry!=null) {
    geometry.setSRID(4326);
}根据经纬度生成geoHash
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;

@Component
public class GeoHashUtil {
    public final double Max_Lat = 90;
    public final double Min_Lat = -90;
    public final double Max_Lng = 180;
    public final double Min_Lng = -180;

    private final String[] base32Lookup = {
            "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "b", "c", "d", "e", "f", "g", "h", "j", "k",
            "m", "n", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"
    };

    /**
   * 根据geoHash串获取中心点经纬度
   * @param geoHashCode
   * @returnlng->xlat->y
   */
    public double[] getSpaceCoordinate(String geoHashCode) {
      if(StringUtils.isBlank(geoHashCode)){
            return new double;
      }
      List<Integer> list = base32Decode(geoHashCode);
      String str = convertToIndex(list);
      GeoHashPoint geoHashPoint = splitLatAndLng(str);
      double y = revert(Min_Lat, Max_Lat, geoHashPoint.getLatList());
      double x = revert(Min_Lng, Max_Lng, geoHashPoint.getLngList());
      return new double[]{x, y};
    }


    /**
   * 根据精度获取GeoHash串
   * @param lng 经度 x
   * @param lat 纬度 y
   * @param precise 精度
   * @return
   */
    public String getGeoHash( double lng, double lat, int precise) {
      // 纬度二值串长度
      int latLength;
      //经度二值串长度
      int lngLength;
      if (precise < 1 || precise > 12) {
            precise = 12;
      }
         latLength = (precise * 5) / 2;
      if (precise % 2 == 0) {
            lngLength = latLength;
      } else {
            lngLength = latLength + 1;
      }
      return encode(lat, lng, latLength, lngLength);
    }

    /**
   * 经纬度二值串合并:偶数位放经度,奇数位放纬度,把2串编码组合生成新串
   *
   */
    public String encode(double lat, double lng, int latLength, int lngLength) {
      if (latLength < 1 || lngLength < 1) {
            return StringUtils.EMPTY;
      }
      List<Character> latList = new ArrayList<>(latLength);
      List<Character> lngList = new ArrayList<>(lngLength);
      // 获取维度二值串
      convert(Min_Lat, Max_Lat, lat, latLength, latList);
      // 获取经度二值串
      convert(Min_Lng, Max_Lng, lng, lngLength, lngList);
      StringBuilder sb = new StringBuilder();
      for (int index = 0; index < latList.size(); index++) {
            sb.append(lngList.get(index)).append(latList.get(index));
      }
//      如果二者长度不一样,说明要求的精度为奇数,经度长度比纬度长度大1
      if (lngLength != latLength) {
            sb.append(lngList.get(lngList.size() - 1));
      }

      return base32Encode(sb.toString());
    }

    /**
   * 将合并的二值串转为base32串
   *
   * @param str 合并的二值串
   * @return base32串
   */
    private String base32Encode(final String str) {
      String unit = "";
      StringBuilder sb = new StringBuilder();
      for (int start = 0; start < str.length(); start = start + 5) {
            unit = str.substring(start, start + 5);
            sb.append(base32Lookup);
      }
      return sb.toString();
    }

    /**
   * 每五个一组将二进制转为十进制
   *
   * @param str 五个为一个unit
   * @return 十进制数
   */
    private int convertToIndex(String str) {
      int length = str.length();
      int result = 0;
      for (int index = 0; index < length; index++) {
            result += str.charAt(index) == '0' ? 0 : 1 << (length - 1 - index);
      }
      return result;
    }


    private void convert(double min, double max, double value, int count, List<Character> list) {
      if (list.size() > (count - 1)) {
            return;
      }
      double mid = (max + min) / 2;
      if (value < mid) {
            list.add('0');
            convert(min, mid, value, count, list);
      } else {
            list.add('1');
            convert(mid, max, value, count, list);
      }
    }



    /**
   * 将二值串转换为经纬度值
   *
   * @param min区间最小值
   * @param max区间最大值
   * @param list 二值串列表
   */
    private double revert(double min, double max, List<String> list) {
      double value = 0;
      double mid;
      if (list.size() <= 0) {
            return (max + min) / 2.0;
      }
      for (String flag : list) {
            mid = (max + min) / 2;
            if ("0".equals(flag)) {
                max = mid;
            }
            if ("1".equals(flag)) {
                min = mid;
            }
            value = (max + min) / 2;
      }
      return Double.parseDouble(String.format("%.6f", value));
    }

    /**
   * 分离经度与纬度串
   *
   * @param latAndLngStr 经纬度二值串
   */
    private GeoHashPoint splitLatAndLng(String latAndLngStr) {
      GeoHashPoint geoHashPoint = new GeoHashPoint();
      // 纬度二值串
      List<String> latList = new ArrayList<>();
       // 经度二值串
      List<String> lngList = new ArrayList<>();
      for (int i = 0; i < latAndLngStr.length(); i++) {
//            奇数位,纬度
            if (i % 2 == 1) {
                latList.add(String.valueOf(latAndLngStr.charAt(i)));
            } else {
//                偶数位,经度
                lngList.add(String.valueOf(latAndLngStr.charAt(i)));
            }

      }
      geoHashPoint.setLatList(latList);
      geoHashPoint.setLngList(lngList);
      return geoHashPoint;
    }

    /**
   * 将十进制数转为五个二进制数
   *
   * @param nums 十进制数
   * @return 五个二进制数
   */
    private String convertToIndex(List<Integer> nums) {
      StringBuilder str = new StringBuilder();
      for (Integer num : nums) {
            StringBuilder sb = new StringBuilder(Integer.toBinaryString(num));
            int length = sb.length();
            if (length < 5) {
                for (int i = 0; i < 5 - length; i++) {
                  sb.insert(0, "0");
                }
            }
            str.append(sb);
      }
      return str.toString();
    }

    /**
   * 将base32串转为合并的二值串
   *
   * @param str base32串
   * @return 合并的二值串
   */
    private List<Integer> base32Decode(String str) {
      List<Integer> list = new ArrayList<>();
      for (int i = 0; i < str.length(); i++) {
            String ch = String.valueOf(str.charAt(i));
            for (int j = 0; j < base32Lookup.length; j++) {
                if (base32Lookup.equals(ch)) {
                  list.add(j);
                }
            }
      }
      return list;
    }

    public static class GeoHashPoint{
      /**
         * 纬度二值串
         */
      private List<String> latList;
      /**
         * 经度二值串
         */
      privateList<String> lngList;

      public List<String> getLatList() {
            return latList;
      }

      public void setLatList(List<String> latList) {
            this.latList = latList;
      }

      public List<String> getLngList() {
            return lngList;
      }

      public void setLngList(List<String> lngList) {
            this.lngList = lngList;
      }
    }

    public static void main(String[] args) {

      GeoHashUtil geoHashUtil = new GeoHashUtil();

      // 根据精度获取GeoHash串
      String geoHash = geoHashUtil.getGeoHash( 120.234133,30.402616, 12);
      System.out.println(geoHash);

      // 根据geoHash串获取中心点经纬度
      double[] spaceCoordinate = geoHashUtil.getSpaceCoordinate(geoHash);
      System.out.println(spaceCoordinate+","+spaceCoordinate);

    }

}
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: PostgreSQL+GeoHash地图点位聚合