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代码中进行融合优化处理。

思路:

  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地图点位聚合的更多相关文章

  1. 百度地图点聚合MarkerClusterer,性能优化

    参考文献:http://www.cnblogs.com/lightnull/p/6184867.html 百度的点聚合算法 是基于方格和距离的聚合算法,即开始的时候地图上没有任何已知的聚合点,然后遍历 ...

  2. 百度地图点聚合MarkerClusterer性能优化

    公司要求做个百度地图点聚合的性能优化,需一次性加载9万条数据. 记录下自己的优化过程.(只想看优化代码的可直接移步:步骤三) 一.引入百度地图 vue项目中,在index.html文件中用script ...

  3. ionic 修改应用名称 、启动页出现黑白屏 及 修改百度离线地图 点聚合 图标

    1.ionic 修改应用名称 2.启动页打开后会在图片消失会出现一小段黑屏的时间 解决方法: 首先,启动页的图片消失时间默认是在config.xml配置的 <preference name=&q ...

  4. [转]百度地图点聚合MarkerClusterer移动地图时,Marker的Label丢失的问题

    参考文献:http://www.cnblogs.com/jicheng1014/p/3143859.html 问题现象: 使用MarkerClusterer_min.js,可以实现点聚合,但是当将带有 ...

  5. Django对postgresql数据库进行分组聚合查询

    action(methods=['GET'], detail=False, url_path='count') def count(self, request): """ ...

  6. postgresql 臭氧8小时聚合函数

    1.定义数据拼接函数 CREATE OR REPLACE FUNCTION "public"."sfun"("results" _numer ...

  7. 利用OpenStreetMap(OSM)数据搭建一个地图服务

     http://www.cnblogs.com/LBSer/p/4451471.html 图 利用OSM数据简单发布的北京地图服务   一.OSM是什么 开放街道图(OpenStreetMap,简称O ...

  8. PGIS大数据量点位显示方案

    PGIS大数据量点位显示方案 问题描述 PGIS在地图上显示点位信息时,随点位数量的增加浏览器响应速度会逐渐变慢,当同时显示上千个点时浏览器会变得非常缓慢,以下是进行的测试: 测试环境: 服务器: C ...

  9. geoserver + postgis+postgresql+agslib.swc

    运用开源的geoserver+postgis+postgresql+arcgis for flex api 开发地图应用系统. 1.Geoserver GeoServer 是 OpenGIS Web ...

  10. 如何在vue单页应用中使用百度地图

    作为一名开发人员,每次接到开发任务,我们首先应该先分析需求,然后再思考技术方案和解决方案.三思而后行,这是一个好的习惯. 需求:本项目是采用vue组件化开发的单页应用项目,现需要在项目中引入百度的地图 ...

随机推荐

  1. This application failed to start because it could not find or load the Qt platforms plugins

     由于一直在linux下操作,今天Qt移植平台的时候导致.exe可执行文件一直运行不起来,提示缺少某些dll库,这个问题解决起来简单(直接去qt源码里面查找对应库添加到可执行文件目录就行),但是之后一 ...

  2. AspNetCoreRateLimit应用于MVC项目求助

    AspNetCoreRateLimit应用于MVC项目求助 前言 之前发过一篇文章: .NET Core WebApi接口ip限流实践 - 妙妙屋(zy) - 博客园 (cnblogs.com) 然后 ...

  3. elSelect点击空白处无法收起下拉框(失去焦点并隐藏)

    学习记录,为了以后有同样的问题,省得再百度了,方便自己也方便你们element 中多选的select 有个问题,就是点击空白或者关闭弹窗,下拉还会一直展示出来百度了好一会,觉得下面两位大佬说的最合理, ...

  4. 数据结构与算法大作业:走迷宫程序(C语言,DFS)(代码以及思路)

    好家伙,写大作业,本篇为代码的思路讲解   1.大作业要求 走迷宫程序 问题描述: 以一个 m * n 的长方阵表示迷宫, 0和1分别表示迷宫的通路和障碍. 设计一个程序, 对任意设定的迷宫, 求出一 ...

  5. SqliLabs 第二关 ,数字型注入!!!

    首先打开网页,进行注入点的测试 输入?id=1 and 1=1发现1=2的时候出现了报错,说明服务器接收了我们的指令,并且进行了反馈,说明了有注入点的存在,然后对注入点进行测试 然后输入order b ...

  6. 2023-01-03:超过5名学生的课。编写一个SQL查询来报告 至少有5个学生 的所有班级,返回结果不限顺序。请问sql语句如何写? +---------+ | class | +-----

    2023-01-03:超过5名学生的课.编写一个SQL查询来报告 至少有5个学生 的所有班级,返回结果不限顺序.请问sql语句如何写? ±--------+ | class | ±--------+ ...

  7. 2021-07-04:股票问题1。给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。你只能选择某一天 买入这只股票,并选择在未来的某一个不同的日子

    2021-07-04:股票问题1.给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格.你只能选择某一天 买入这只股票,并选择在未来的某一个不同的日子 ...

  8. vue全家桶进阶之路50:Vue3 环境变量+跨域设置实例

    使用.env加后缀的方式来建立某个模式下的环境变量, 例如:项目根目录新建两个环境变量文件(development开发环境和production生产环境): .env.development .env ...

  9. pages.json 文件:自定义导航栏

    自定义导航栏使用注意 当navigationStyle设为custom或titleNView设为false时,原生导航栏不显示,此时要注意几个问题: 非H5端,手机顶部状态栏区域会被页面内容覆盖.这是 ...

  10. SQL Server2019 删除列字段

    命令: 有默认值时用:alter table 表名 DROP  约束 alter table 表名 DROP COLUMN 列名 例如: alter table LJEL005H DROP COLUM ...