PostgreSQL+GeoHash地图点位聚合

这篇具有很好参考价值的文章主要介绍了PostgreSQL+GeoHash地图点位聚合。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

PG数据库安装扩展

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

CREATE EXTENSION postgis;
CREATE EXTENSION postgis_topology;
CREATE EXTENSION postgis_geohash;

GeoHash

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

  1. 将经纬度变成二进制
  2. 将经纬度的二进制合并
  3. 通过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
     * @return  lng->x  lat->y
     */
    public double[] getSpaceCoordinate(String geoHashCode) {
        if(StringUtils.isBlank(geoHashCode)){
            return new double[2];
        }
        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[convertToIndex(unit)]);
        }
        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[j].equals(ch)) {
                    list.add(j);
                }
            }
        }
        return list;
    }

    public static class GeoHashPoint{
        /**
         * 纬度二值串
         */
        private List<String> latList;
        /**
         * 经度二值串
         */
        private  List<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[0]+","+spaceCoordinate[1]);

    }

}

数据库生成geometry和geoHash

当应用中对数据进行新增修改操作时,可以在代码中生成对应的geometry和geoHash字段的值。但有时候数据不在应用中录入,直接由数据工程师写入的话,就会出现:
① 经纬度新增了但是geometry和geoHash字段的值为空

② 经纬度更新了但是没有更新geometry和geoHash字段的值

解决:

① 让数据工程师在写入经纬度的同时帮你存入或更新geometry和geoHash字段的值

② 自己手动执行sql语句,重新生成geometry和geoHash字段的值

③ 基于第2步,为表创建触发器,当对表进行insert或update(update更新经纬度字段)操作时,会自动存入或更新geometry和geoHash字段的值

两个相关函数

① ST_GeomFromText 函数

示例:ST_GeomFromText('POINT(120.1307732446746 30.2678227400894)', 4326)

说明:该函数返回经纬度对应的Geometry对象

② st_geohash 函数

示例:st_geohash(ST_GeomFromText('POINT(120.1307732446746 30.2678227400894)', 4326))

说明: 该函数返回经纬度对应的geoHash值

手动执行sql

手动执行sql, 查询所有经纬度不为空的数据,然后更新每条数据的geometry和geoHash字段的值

-- 1. 函数:更新每条数据的geometry和geoHash字段的值
create or replace function func_update_geodata() returns text
as $$

declare
    rec record;

begin

    -- 遍历所有经纬度不为空的数据
    for rec in select * from 表名 where 经纬度 is not null and 经纬度 != ''
    LOOP

        update 表名 set pgis_geometry = st_geomfromtext('POINT('|| longitude ||' '|| latitude ||')', 4326),
                             pgis_geohash = st_geohash(st_geomfromtext('POINT('|| longitude ||' '|| latitude ||')', 4326))
        where id = rec.id;

    END LOOP;

    return 'success';

end;
$$ language plpgsql;

-- 2. 调用
select func_update_geodata();
触发器生成geometry和geoHash
-- 1. 创建触发器函数
create or replace function func_generate_geodata_to_mytab() returns trigger as $body$

    begin

        update 表名 set pgis_geometry = st_geomfromtext('POINT('|| longitude ||' '|| latitude ||')', 4326),
                             pgis_geohash = st_geohash(st_geomfromtext('POINT('|| longitude ||' '|| latitude ||')', 4326))                  
        where id = NEW.id;

        RETURN NEW;

    end;
$body$ language plpgsql;

-- 2. 创建触发器
create trigger trigger_generate_geodata_to_mytab
after insert or update of 经纬度 on 表名
    for each row execute procedure func_generate_geodata_to_mytab();

聚合查询

使用JPA的原生sql查询,@Query(nativeQuery = true, value="sql语句")

查询聚合数据

-- 查询聚合数据
select t.geohash                            as geohash,
       st_x(st_pointfromgeohash(t.geohash)) as longitude,
       st_y(st_pointfromgeohash(t.geohash)) as latitude,
       t.count                              as aggregationCount
from (
         select left(pgis_geohash, ?2) as geohash, count(*) as count
         from 表名
         where pgis_geohash is not null
           and pgis_geohash != ''
           and case when ?1 != '' then st_contains(st_geometryfromtext(?1, 4326), pgis_geometry) else 1 = 1 end
         group by geohash) t;
         
/*
1. 【?1】为页面传来的Wkt数据
2. 【?2】为从左边截取geohash的前几位
3. st_x(st_pointfromgeohash('geoHash的值')) 、st_y(st_pointfromgeohash('geoHash的值')) 根据geoHash的值获取聚合后的中心点坐标
*/

查询聚合详情

-- 查询聚合详情
select *
from 表名
where pgis_geohash is not null and pgis_geohash != ''
  and left(pgis_geohash, ?2) in (?1);

/*
1. 【?1】为geohash值的集合
2. 【?2】为从左边截取geohash的前几位
*/

优化

geoHash目前聚合后发现在地图上展示效果不好,聚合点在地图上横竖规律排布,因此聚合后我们可以在java代码中进行融合优化处理。

思路:文章来源地址https://www.toymoban.com/news/detail-609391.html

  1. 将聚合后的每组聚合点里的点相加,然后除以聚合点的数量得出一个平均值(可以根据情况在这个平均数上乘以一个比例)
  2. 遍历聚合的list,将大于等于平均值的聚合点和小于平均值的聚合点拆开放在两个集合里(分别为A和B)
  3. 遍历小于平均值的聚合点集合(A),找到与当前点距离最近的高于平均数的一个聚合点b,把a融合至B
  4. 遍历B,重新计算并设置融合后的经纬度
/**
 * @param list 聚合查询的结果
 * @return     优化后的聚合结果
 */
public List optimizationAggregation(List list){
    
	// 所有聚合点数量
    long sum = list.stream().mapToLong(T::getCount).sum();
    // 获取平均数
    long average = sum / list.size();
   
	List bigList = new ArrayList<>();
    List smallList = new ArrayList<>();
    
    for (T item : list) {
        if (item.getCount() < average) {
            smallList.add(item);
        } else {
            bigList.add(item);
        }
    }
    
    Map<T, List<T>> map = new HashMap<>();
    for(T item : bigList){
        map.put(item, new ArrayList<>());
    }
    
    for(T smallItem : smallList){
        PGpoint smallPoint = smallItem.getGeoPoint();
        
        int index = -1;
        // 在bigList找出距离当前聚合点最近的点
        double minDistance = Double.MAX_VALUE;
        for(int i = 0; i < bigList.size(); i++){
            
            T bigItem = bigList.get(i);
            PGpoint bigPoint = bigItem.getGeoPoint();
            
            double distance = GeometryUtil.getDistance(smallPoint.x, smallPoint.y, bigPoint.x, bigPoint.y);
            if(distance >= minDistance){
                continue;
            }
            minDistance = distance;
            index = i;
        }

        T bigItem = bigList.get(index);
        List<T> childList = map.get(bigItem);
        if(null == childList){
            childList = new ArrayList<>();
        }
        childList.add(smallItem);
        
        map.put(bigItem, childList);
    }
    
    // 结果
    List<T> result = new ArrayList<>();
    map.forEach((key, value)->{
        
        PGpoint parentPoint = key.getGeoPoint();
        
        value = value.stream().sorted(Comparator.comparing(T::getCount, Comparator.reverseOrder())).collect(Collectors.toList());
        for(T childItem : value){
            PGpoint childPoint = childItem.getGeoPoint();
            
            double difX = parentPoint.x-childPoint.x;
            double difY = parentPoint.y-childPoint.y;
            double x = parentPoint.x - (new BigDecimal(difX * childItem.getCount()).divide(new BigDecimal(key.getCount()), 15, RoundingMode.HALF_DOWN).doubleValue());
            double y = parentPoint.y - (new BigDecimal(difY * childItem.getCount()).divide(new BigDecimal(key.getCount()), 15, RoundingMode.HALF_DOWN).doubleValue());
            
            PGpoint pGpoint = new PGpoint(x, y);
            key.setGeoPoint(pGpoint);
            key.setLongitude(String.valueOf(x));
            key.setLatitude(String.valueOf(y));

            key.setCount(key.getCount() + childItem.getCount());

            if(null == key.getGeohashSet()){
                key.setGeohashSet(new HashSet<>());
            }
            key.getGeohashSet().add(childItem.getGeohash());
        }

        result.add(key);
    });

    return result;
    
    
}

到了这里,关于PostgreSQL+GeoHash地图点位聚合的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • MySQL数据库,聚合查询

    目录 1、聚合查询 1.1聚合函数 1.1.1count函数 1.1.2sum函数 1.1.3avg函数 1.1.4max函数 1.1.5min函数 1.2group by子句 1.3having 聚合函数查询又叫函数查询,它是通过一些特定的函数根据需求来查询相关的信息,常见的聚合函数为: COUNT - 求总数,不包含某字段为null值 SUM - 求和,不包含某字

    2023年04月21日
    浏览(58)
  • 【MySql】数据库的聚合查询

    写在最前面的话         哈喽,宝子们,今天给大家带来的是MySql数据库的聚合查询。在前面CRUD章节我们学习了表达式查询,表达式查询是针对列和列之间进行运算的,那么如果想在行和行之间进行运算,那么就需要用到聚合查询。 聚合查询除了包含聚合函数外(count,

    2024年02月09日
    浏览(58)
  • postgresql数据库定时备份到远程数据库

    1.老规矩,服务器目录结构: conf目录无内容 profile: 其中: 最后一行 export PGPASSWORD=‘root’ 是需要备份的数据库的密码,因为直接用 pg_dump 命令备份需要输入密码交互,而我们需要达到自动备份,所以借助这种方式不需要输入密码 docker-compose.yml: 启动容器: 然后再data目录下面

    2024年02月09日
    浏览(48)
  • 【数据库】什么是 PostgreSQL?开源数据库系统

    PostgreSQL 是一个开源的对象关系数据库系统,本文,我们将讨论 PostgreSQL、它的用途和好处。 PostgreSQL 是由 PostgreSQL Global Development Group 开发的高级 开源关系数据库管理系统(RDBMS) 。它作为 POSTGRES 项目的一部分于 1986 年在加州大学伯克利分校启动,它最初于 1996 年 7 月 8 日发布

    2023年04月08日
    浏览(48)
  • PostgreSQL Linux操作PostgreSQL数据库

    PostgreSQL教程 菜鸟教程:https://www.runoob.com/postgresql/postgresql-tutorial.html 登录PG数据库:psql -U 用户名(U需要大写) 登录PG数据库(指定主机、端口,并进入指定数据库): psql -U 用户名 -h 127.0.0.1 -p 5432 -d 数据库名 -U 登录的用户名 -h 连接的主机(默认127.0.0.1,可替换成远程主机

    2024年02月11日
    浏览(64)
  • 探索存证、溯源类数据库最优解,聚合数据区块链数据库AnchorDB发布

    近日,聚合数据区块链数据库AnchorDB 正式对外发布,这是企业对于数字化技术应用的又一次探索,产品的发布,将为存证、溯源类场景提供更高效、易用的数据库解决方案,并且进一步丰富聚合数据的数字化产品矩阵。 作为一款具有区块链不可篡改特性的轻量级存证数据库,

    2024年02月11日
    浏览(46)
  • [运维|数据库] docker postgresql数据库环境变量配置

    要配置Docker中的PostgreSQL数据库的环境变量,可以使用以下方法: 使用Docker命令行: 将 用户名 , 密码 , 数据库名 替换为你想要设置的实际值。这将创建一个名为 mypostgres 的容器,并将 PostgreSQL 的用户名、密码和数据库名设置为指定的值。 -p 5432:5432 指定了容器内部和主机之间

    2024年02月09日
    浏览(66)
  • 数据库新闻速递 -- POSTGRESQL 正在蚕食数据库市场 (翻译)

    开头还是介绍一下群,如果感兴趣polardb ,mongodb ,mysql ,postgresql ,redis 等有问题,有需求都可以加群群内有各大数据库行业大咖,CTO,可以解决你的问题。加群请加 liuaustin3微信号 ,在新加的朋友会分到3群(共1140人左右 1 + 2 + 3) 尽管NoSQL数据库继续蓬勃发展,但关系型数据库仍

    2024年02月13日
    浏览(63)
  • PostgreSQL-数据库命令

    一个数据库是一个或多个模式的集合,而模式包含表、函数等。因此,完整的逻辑组织结构层次是服务器实例(PostgreSQL Server)、数据库(Database)、模式(Schema)、表(Table),以及某些其他对象(如函数)。一个PostgreSQL服务器实例可以管理多个数据库。当应用程序连接到一

    2024年02月14日
    浏览(53)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包