Gis分析 POI空间聚合研究Demo实现
2010年06月09日
前些时候领导让我们提前预言商业智能gis分析以及物流交通相关的业务。其中有个重要的技术就是 POI空间聚合。从网上找了些资料,大多是E文的,而且说的都巨复杂,不是特别容易理解。
多亏了google,哥们后来无意中发现了一个esri做的一个聚合示例。 http://www.esrichina-bj.cn/market/H1N1/H1N1.swf ,哈哈如获至宝,通过httpwatch找到数据来源,发现他是通过客户端做聚合运算的。flex的引擎下几百个点的运算速度相当不错。而且聚合的效果也相当的明显。唯一不足的是,他是相对的屏幕坐标进行聚合运算的。也就是最大也就是1440*900(呵呵这是我的电脑分辨率)。所以每次移动屏幕,聚合的点都会因为吸收或者抛弃屏幕外新的POI而发生聚合变化,变来变去给人的感受不是很爽。而且从分析来看,这种运算是根据每次拖动地图的px来进行,也就是稍稍的拖动一次地图就会有大规模的聚合运算(代表大的for循环)。当然如果用js来实现的话可远远达不到这个效果。
有了参考的样本和示例,呵呵,信心增强了不少。这世界上最难得就是创新,只要见别人实现过,研究研究就有门。
注:gis圈小的很,正好有个特好的哥们在esir(哈,我们以前还一个宿舍住过呢) 趁机要了些算法看了看。
决定用flex试试看以前没有用过flex,初次接触起来感觉也还凑活,有点像java和js的结合体,修改修改算法也没啥难的。导入eclips,算法还是基本都能看明白的。我梳理了一遍,也不是很复杂。
我对算法做了一部分改进。目的是解决拖动时候,聚合不停变化,给人不舒服的感觉,其实也就做了3步:
1、去掉了拖动地图聚合运算相应。改为了缩放比例尺时请求运算,这样能增加效率,甚至日后可以都放到服务端算提前聚合,这样速度就快多了。
2、聚合时用到的相对屏幕坐标,改为相对比例尺的原点坐标。这样就不会变来变去了。
3、更换了我们的flex 地图api 显示模式 ,增加了部分功能。
呵呵,个人感觉比esir做的要好。改天要些大数据,搞个厕所分布图。
先给大家来点直观的插图:
点击可查看聚合得点。以及POI的位置,这点学spaclateKey的呵呵。
聚合前 3个聚合圆
聚合后。呵呵,增加了比例尺缩放效果后非常明显,能看到相邻的点快速进行了合并。
其实,空间聚合所白了也就以下几步骤,有点基础的朋友相信看看就会明白的。 不清楚email给我哈。
第一步、针对基础数据进行初步聚合。
遍历POI数据转化为相对比例尺原点的屏幕坐标(要保证每个POI的屏幕数据都是唯一的),这里还不能用经纬度,因为经纬度相对比例尺来说都是固定的。但是的原点屏幕坐标相对原点是相同的。但不同比例尺下不同。循环过程中根据聚合的直径范围判断将POI放入不同的聚合数组对象中,并计算这些POI点的中心点(根据算法,他会像点多放偏移)。这样就初步得到多个聚合数组,并且记录的聚合中心点也是相对POI数量有侧重的。
相对的Flex代码如下: private function assignMapPointsToClusters() : void { m_orig = new Dictionary(); //循环所有POI for each ( var tcar:TCar in sink ) { //经纬度转屏幕像素(固定屏幕像素) var sxy:SE_Point = getPixelCoord(tcar.lnglat); //根据直径的精度,进行聚合 Convert to cluster x/y values. var cx : int = sxy.x / m_diameter; var cy : int = sxy.y / m_diameter; //cx位移19位和cy组成Key cluster dictionary key. //var ci : int = (cx 地图上显示来说还是比较杂乱比较多。我们需要再次针对这些聚合圆进行聚合。根据四面八方的原则,再聚合圆的基础上在进行聚合直径的扩充。如果发现邻近的聚合圆存在就合并,并根据数量偏移聚合圆的位置(当然靠近数量多的一方哈)。这个循环直到发现每个聚合圆的直径都没有接触到附近的聚合圆为止。
相对应的Flex如下: /** * 1.2判断相邻的两个聚合圆是否也需要聚合。 */ private function mergeOverlappingClusters() : void { m_overlapExists = false; // 建立一个新的dic存储圆圆聚合后的圆 Create a new set to hold non-overlapping clusters. const dest/**/ : Dictionary = new Dictionary(); // 循环所有聚合圆,进行再聚合 for each( var cluster : Cluster in m_orig ) { //忽略空的聚合数组 if( cluster.n === 0 ) { continue; } //8个方向寻找邻近可聚合的圆 Search all immediately adjacent clusters. searchAndMerge( cluster, 1, 0 ); searchAndMerge( cluster, -1, 0 ); searchAndMerge( cluster, 0, 1 ); searchAndMerge( cluster, 0, -1 ); searchAndMerge( cluster, 1, 1 ); searchAndMerge( cluster, 1, -1 ); searchAndMerge( cluster, -1, 1 ); searchAndMerge( cluster, -1, -1 ); //得到新的聚合圆 Find the new cluster centroid values. var cx : int = cluster.x / m_diameter; var cy : int = cluster.y / m_diameter; cluster.cx = cx; cluster.cy = cy; //var ci : int = (cx flex做主要是为了方便调试,因为一会还要用新的 flexapi做地图显示。
第三步,范围判断显示聚合圆
剩下的就最简单多了。你可以服务端来算、也可以客户端来算。就是根据屏幕经纬度区间去获取需要显示的聚合圆显示。因为我们对屏幕做了聚合,所以一般情况下,聚合圆的数量不会太多,当然如果18级别,你又是几十万的数据那您还是放到服务端好了。也不用oracle或者postSql的spatial。直接内存循环速度就没有问题。 /** * 2、根据屏幕范围显示绘制圆图 **/ public function showClustersOnMap():void { var bounds:SE_LngLatBounds = map.getLngLatBounds();//屏幕四角坐标 //循环聚合点,显示与地图上 for each ( var cluster : Cluster in m_orig ){ if (bounds.containsLngLat(new SE_LngLat(cluster.lng, cluster.lat))) { var cs:ClusterSymbol = new ClusterSymbol(cluster.tcars); cs.radius = cluster.n > 1 ? 20 : 8; //如果数量大于一则画大圆,否则小圆 cs.map = map; cs.draw(cluster); cs.lnglat.lng = cluster.lng; cs.lnglat.lat = cluster.lat; map.addOverLay(cs); source.addItem(cs); //图形增加到聚合数组中 cs.addEventListener(SE_OverlayMouseEvent.SE_OVERLA Y_MOUSE_CLICK, clusterClickedHandler); } } }