最近用了下https://github.com/spatial4j/spatial4j spatial4j 做地理位置的搜索。
其中给定两个点的经纬度计算距离的公式有3种:
Haversine http://en.wikipedia.org/wiki/Haversine_formula
Law of Cosines(余弦定理) http://en.wikipedia.org/wiki/Spherical_law_of_cosines
Vincenty http://en.wikipedia.org/wiki/Vincenty's_formulae
spatial4j 默认使用的是Haversine
其中根据http://stackoverflow.com/questions/2096385/formulas-to-calculate-geo-proximity/ 的讨论
从速度上讲: Law of Cosines > Haversine > Vincenty
然后我就用了 Law of Cosines,结果发现比另两个都慢。。。
这不科学啊,然后我就开始看code: https://github.com/spatial4j/spatial4j/blob/master/src/main/java/com/spatial4j/core/distance/DistanceUtils.java
里面的3个函数distHaversineRAD, distLawOfCosinesRAD 和 distVincentyRAD
给定的地球表面的两点,这三个函数都是计算球心到这两点的向量的夹角,有了这个夹角的话乘以地球半径就是距离。
把里面的3个函数拿出来跑测试,用同一组100万中国经纬度范围longitude (73, 135) latitude (3, 53)
的随机数据在我的MacBookPro测试一下:
distHaversineRAD cost: 193.561ms
distLawOfCosinesRAD cost: 407.285ms
distVincentyRAD cost: 264.404ms
distLawOfCosinesRAD 果然最慢的啊,仔细看了下这个函数。
public static double distLawOfCosinesRAD(double lat1, double lon1, double lat2, double lon2) {
// Check for same position
if (lat1 == lat2 && lon1 == lon2)
return 0.0;
// Get the m_dLongitude difference. Don't need to worry about
// crossing 180 since cos(x) = cos(-x)
double dLon = lon2 - lon1;
double a = DEG_90_AS_RADS - lat1;
double c = DEG_90_AS_RADS - lat2;
double cosB = (Math.cos(a) * Math.cos(c))
+ (Math.sin(a) * Math.sin(c) * Math.cos(dLon));
// Find angle subtended (with some bounds checking) in radians
if (cosB < -1.0)
return Math.PI;
else if (cosB >= 1.0)
return 0;
else
return Math.acos(cosB);
}
优化1:
直觉告诉我acos这个计算反余弦的肯定最耗时。其实在搜索中使用距离函数最多的场景是做Filter用,比如给定中心点的经纬度搜索10公里范围内的人。
假设中心点经纬度是(lon1, lat1),距离d,地球半径为r,要判断的点是(lon2, lat2)
spatial4j中判断是否在范围内的做法:distLawOfCosinesRAD(lat1, lon1, lat2, lon2) < d/r
注意到Cosine在[0, PI]之间是递减的,任意两个向量的夹角也是在[0, PI]之间,所以如果判断距离是不是在范围内,可以不计算出来角度, 只计算出Cosine即可。
Math.cos(distLawOfCosinesRAD(lat1, lon1, lat2, lon2)) > Math.cos(d/r)
其中Math.cos(d/r)只需要计算一次,存储下来。
这样做距离Filter用的时候,distLawOfCosinesRAD 变成了这个样子
public static double distLawOfCosinesRAD(double lat1, double lon1, double lat2, double lon2) {
if (lat1 == lat2 && lon1 == lon2) return 0.0;
// Get the m_dLongitude difference. Don't need to worry about
// crossing 180 since cos(x) = cos(-x)
double dLon = lon2 - lon1;
double a = DEG_90_AS_RADS - lat1;
double c = DEG_90_AS_RADS - lat2;
double cosB = (Math.cos(a) * Math.cos(c)) + (Math.sin(a) * Math.sin(c) * Math.cos(dLon));
return cosB;
}
distLawOfCosinesRAD cost: 79.048ms
耗时变成了原来的1/5。
优化2:
又看了一下这个函数,作为处女座的人完全无法接收这两行啊:
double a = DEG_90_AS_RADS - lat1;
double c = DEG_90_AS_RADS - lat2;
其中 DEG_90_AS_RADS = Math.PI / 2;
显然Math.cos(a) = Math.sin(Math.PI / 2 - a) 所以干掉这两行后,distLawOfCosinesRAD 变成了这个样子
public static double distLawOfCosinesRAD(double lat1, double lon1, double lat2, double lon2) {
// Check for same position
if (lat1 == lat2 && lon1 == lon2) return 0.0;
// Get the m_dLongitude difference. Don't need to worry about
// crossing 180 since cos(x) = cos(-x)
double dLon = lon2 - lon1;
double cosB = (Math.sin(lat1) * Math.sin(lat2)) + (Math.cos(lat1) * Math.cos(lat2) * Math.cos(dLon));
return cosB;
}
distLawOfCosinesRAD cost: 36.258ms
又减少了一半。这个下降的幅度显然不合理,因为只是少做了两次double减法。感觉这个提高是和数据分布有关系,在中国的经纬度范围内效果很好,其实在美国的经纬度范围longitude (-75,-120) latitude (30, 50) 效果也很好。
优化3:
一次搜索中,中心点的经纬度(lon1, lat1)是不变的,但是每次计算距离我们都要计算Math.sin(lat1) 和 Math.cos(lat1), 如果把Math.sin(lat1) 和 Math.cos(lat1) 只计算一次每次计算距离时作为参数直接传入,那么会各减少一次Math.sin 和 Math.cos 计算。
distLawOfCosinesRAD cost: 24.356ms
减少了10多ms,这个优化效果不明显了。
如果还要继续优化:
假设每次搜索过程中需要和中心点(lon1, lat1)比较的点(lon2, lat2)的集合只取决于索引(比如每个用户的经纬度),那么我们在建索引时可以将经纬度(lon, lat)
转为空间坐标系(x, y, z)比如可以这样映射:
x = Math.cos(lat) * Math.cos(lon);
y = Math.cos(lat) * Math.sin(lon);
z = Math.sin(lat);
然后把(x, y, z)建入索引。
那么当我们求向量夹角时,只需要做一次点乘操作。比如求(lon1, lat1)和 (lon2, lat2)的夹角,只需要计算
x1*x2 + y1*y2 + z1*z2 这样避免了做任何三角函数换算这个效率是最高的,做100万次这样的操作基本在10ms以内。
Bobo的GeoFacetFilter就是这样做的。https://github.com/senseidb/bobo/blob/master/bobo-browse/src/main/java/com/browseengine/bobo/facets/filter/GeoFacetFilter.java
其实
x1*x2 + y1*y2 + z1*z2
= Math.cos(lat1) *Math.cos(lon1)*Math.cos(lat2) * Math.cos(lon2) +
Math.cos(lat1) *Math.sin(lon1) * Math.cos(lat2)*Math.sin(lon2) +
Math.sin(lat1)*Math.sin(lat2)
= Math.cos(lat1)*Math.cos(lat2)*(Math.cos(lon1) * Math.cos(lon2) + Math.sin(lon1)*Math.sin(lon2)) + Math.sin(lat1)*Math.sin(lat2)
= Math.cos(lat1)*Math.cos(lat2)*Math.cos(lon1-lon2) + Math.sin(lat1)*Math.sin(lat2)
这个结果和distLawOfCosinesRAD完全等价。
由于计算距离的耗时只是整个搜索耗时中的一部分,所以第一个优化在线上体现最明显,第二个不太明显,第三个基本属于过度优化了。
分享到:
相关推荐
赠送jar包:spatial4j-0.6.jar; 赠送原API文档:spatial4j-0.6-javadoc.jar; 赠送源代码:spatial4j-0.6-sources.jar; 赠送Maven依赖信息文件:spatial4j-0.6.pom; 包含翻译后的API文档:spatial4j-0.6-javadoc-...
赠送jar包:spatial4j-0.6.jar; 赠送原API文档:spatial4j-0.6-javadoc.jar; 赠送源代码:spatial4j-0.6-sources.jar; 赠送Maven依赖信息文件:spatial4j-0.6.pom; 包含翻译后的API文档:spatial4j-0.6-javadoc-...
spatial4j官方发布以及maven发布的版本都是基于jdk1.7编译的,碰到jdk1.6的项目会报unsupported major.minor version 51.0错误。这个资源是我基于jdk1.6编译的,执行测试案例都通过了。
spatial4j-0.4.1.jar
如果您正在使用空间网格平方索引方案(无论是还是自定义的东西),那么您很可能会从Spatial4j中找到特别有用的工具。 Spatial4j经过了良好的测试; 通过持续集成(以及另一个Hudson构建)对其进行监视,我们使用...
spatial4j, Java的地理空间库 Spatial4j ( 注:spatial4j主页的官方版本位于 LocationTech: 但是,这个自述文件包含了更丰富的信息。Spatial4j是一个通用的空间/地理空间 ASL 许可的开放源代码Java库。 它的核
"使用Oracle Spatial对ArcSDE中的SDO_GEOMETRY类型数据进行空间操作" Oracle Spatial 是 Oracle 数据库中的一个空间数据处理组件,用于存储、管理和操作空间数据。ArcSDE 是一个空间数据引擎,用于存储和管理大规模...
原理部分会深入探讨Oracle Spatial如何使用空间索引、空间函数和空间操作符来提升查询效率,而技术支持部分则会涉及具体的优化策略,如如何选择合适的索引类型、如何构建和维护空间索引、以及如何利用Oracle提供的...
- **标题**:《空间逻辑手册》(Handbook of Spatial Logics) - **编辑**:Marco Aiello(格罗宁根大学)、Ian Pratt-Hartmann(曼彻斯特大学)与Johan van Benthem(阿姆斯特丹大学) - **出版社**:Springer - **...
为了使用 Hibernate Spatial 4,需要在classpath中添加以下库:hibernate、JDBC 驱动程序和 hibernate-spatial.jar,以及所有 transitive 依赖项。 二、 配置 Hibernate Spatial 要使用 Hibernate Spatial,需要在...
为了优化性能,可以考虑使用空间索引,如MySQL的`SPATIAL`索引,或者将距离计算移到应用程序层。 总的来说,通过理解和应用哈弗辛公式,以及创建相应的MySQL函数,我们可以有效地在数据库中处理基于经纬度的地理...
part 2 共两部分 Handbook of Applied Spatial Analysis Software Tools, Methods and Applications
开发者可以使用 SDO API(Spatial Data Object API)来执行复杂的空间查询,例如找出距离某个点最近的设施,或者找出跨越特定区域的对象。 `sdotype.jar`:这个库包含了 Oracle Spatial 的数据类型定义,比如几何...
Oracle Spatial OCI源码是Oracle数据库中用于处理空间数据的一个组件,它是Oracle Call Interface(OCI)的扩展,专门设计用于在Oracle Spatial中执行高效的空间查询和操作。OCI是Oracle数据库提供的一种C语言编程接口...
在本示例中,我们将探讨如何使用特定工具将 ShapeFile 数据导入到 SqlServer spatial 中。 ShapeFile 是一种常见的矢量地理数据格式,通常由 .shp(形状文件)、.dbf(属性数据)和 .shx(索引文件)等几个文件组成...
许多组织正在转向使用Oracle Spatial作为其核心数据库系统,这主要是因为Oracle Spatial能够提供强大的空间数据存储与处理能力。在这一背景下,如何实现ArcGIS与Oracle Spatial的有效集成成为了一个重要的课题。本文...
除了基本的数据存储之外,Oracle Spatial还提供了丰富的函数和操作符,可以用于执行空间数据之间的各种运算,如距离计算、空间关系判断等。这些操作使得开发者可以在SQL语句中直接处理空间数据,而无需编写额外的...
### Beyond Bags of Features: Spatial Pyramid Matching for Recognizing Natural Scene Categories #### 概述 本文介绍了一种基于近似全局几何对应的方法来识别自然场景类别。该技术通过将图像分割成越来越精细的...
Oracle Spatial 是 Oracle 数据库的一个重要组件,专门用于处理地理空间数据和执行空间分析。这个官方文档是英文版,对于非英语母语者来说可能阅读起来有些困难,但它是理解 Oracle Spatial 功能和操作的关键资源。...