细读百度地图点聚合源码(上)

首先给大家推荐一下我老师大神的人工智能教学网站。教学不仅零基础,通俗易懂,而且非常风趣幽默,还时不时有内涵黄段子!点这里可以跳转到网站

之前在项目中需要用到百度地图的点聚合,看了百度提供的demo之后,稍微读了一些源码就能达到需求了,所以并未深入解读源码。

最近有空就把百度实现点聚合的源码从里到外仔细研究了一遍受益良多,在此分享一下。

为了方便研究我把百度demo中点聚合相关的类抽出来,新建了个工程,有需要可以下载来研究。ClusterDemo 

整个源码分析过程我分为三个部分:

1.整体结构分析

2.核心算法分析

3.实现点聚合

本篇为上篇,主要分析1,2部分。之后还会有个下篇,着重分析具体如何实现marker点聚合以及一些动画处理,这一部分百度处理的非常精妙。

废话少说,进入正文。

1.整体结构

先上一张思维导图:

根据上图我们可以知道,要实现点聚合功能主要分为两部分:

一个是ClusterItem接口,需要我们提供一个实现类,此类主要用来生成地图最终显示的marker,所以包含了经纬度坐标,marker的icon图标,

以及一些其他我们需要保存在marker中的信息。

看代码,这是demo实现的一个ClusterItem的实现类

    /**     * 每个Marker点,包含Marker点坐标以及图标     */    public class MyItem implements ClusterItem {        private final LatLng mPosition;         public MyItem(LatLng latLng) {            mPosition = latLng;        }         @Override        public LatLng getPosition() {            return mPosition;        }//返回marker的坐标         @Override        public BitmapDescriptor getBitmapDescriptor() {//返回marker的图标            return BitmapDescriptorFactory                    .fromResource(R.drawable.icon_gcoding);        }    }

比较简单,就不多说了。

另一个是ClusterManager,从图中可以看到它实现了两个百度地图的接口,一个是监听地图状态变化(地图缩放时,执行点聚合),另一个是监听marker的点击事件。

所以在Demo中给地图设置监听接口时,必须设置成我们的ClusterManager. 然后,在ClusterManager中有一个ClusterTask任务线程,由cluster()方法启动这个线程。

这个线程会在后台执行核心算法将所有的ClusterItem根据一个给定的距离范围进行分组,返回一个cluster集合,这个集合里面的每个cluster都包含若干个ClusterItem,并且每个cluster都达到了进行聚合操作的要求。

而返回的cluster集合,就交由一个Renderer类进行处理。这个类实现了ClusterRenderer接口,主要负责处理各个marker的显示以及它们的的动画。这部分留在

下一篇文章再细说。

另外,ClusterManager中提供了MarkerClickListener接口。

这里贴一下demo中对BaiduMap的一些初始化设置,跟ClusterManager有关。

@Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_marker_cluster_demo);        mMapView = (MapView) findViewById(R.id.bmapView);        ms = new MapStatus.Builder().target(new LatLng(39.914935, 116.403119)).zoom(8).build();        mBaiduMap = mMapView.getMap();        mBaiduMap.setOnMapLoadedCallback(this);        mBaiduMap.animateMapStatus(MapStatusUpdateFactory.newMapStatus(ms));        // 定义点聚合管理类ClusterManager        mClusterManager = new ClusterManager<MyItem>(this, mBaiduMap);        // 添加Marker点        addMarkers();        // 设置地图监听,当地图状态发生改变时,进行点聚合运算        mBaiduMap.setOnMapStatusChangeListener(mClusterManager);     }

这是Demo的Activity的onCreate函数的内容。

我们看到new了一个ClusterManager,并且设置了mBaiduMap的Listener,也就是说ClusterManager能够监听到地图状态的改变包括缩放,旋转等等。

值得一提的是addMarkers()函数,这里也贴一下吧。

    /**     * 向地图添加Marker点     */    public void addMarkers() {        // 添加Marker点        LatLng llA = new LatLng(39.963175, 116.400244);        LatLng llB = new LatLng(39.942821, 116.369199);        LatLng llC = new LatLng(39.939723, 116.425541);        LatLng llD = new LatLng(39.906965, 116.401394);        LatLng llE = new LatLng(39.956965, 116.331394);        LatLng llF = new LatLng(39.886965, 116.441394);        LatLng llG = new LatLng(39.996965, 116.411394);         List<MyItem> items = new ArrayList<MyItem>();        items.add(new MyItem(llA));        items.add(new MyItem(llB));        items.add(new MyItem(llC));        items.add(new MyItem(llD));        items.add(new MyItem(llE));        items.add(new MyItem(llF));        items.add(new MyItem(llG));         mClusterManager.addItems(items);

其实很简单,就是创建一些我们的MyItem对象,然后添加到ClusterManager中。但是这个添加过程其实是比较复杂的,就跟核心算法有关了,下面会分析。

如果不去管核心算法和Renderer类的细节,ClusterManager类还是很简单的,这里就不贴代码了。有兴趣的可以去看一看源码。

2.核心算法

下面我们来分析百度工程师是如何判断这些marker该不该聚合的。

首先我们来看上面提到的ClusterManager的addItems()方法

public void addItems(Collection<T> items) {        mAlgorithmLock.writeLock().lock();        try {            mAlgorithm.addItems(items);        } finally {            mAlgorithmLock.writeLock().unlock();        }    }

我们看到它内部做了同步处理,然后这些ClusterItem是添加到mAlgorithm对象中的。

那么mAlgorithm是什么东西呢?

根据名字我们也能知道,这个就是算法类了。

ClusterManager是这样定义mAlgorithm的:

mAlgorithm = new PreCachingAlgorithmDecorator<T>(new NonHierarchicalDistanceBasedAlgorithm<T>());

这个对象是在ClusterManager的构造函数中初始化的,我们看到其实这里有两个类

PreCachingAlgorithmDecorator类和NonHierarchicalDistanceBaseAlgorithm类

前一个是个装饰类,主要负责管理缓存的一些操作,真正的算法是在NonHierarchicalDistanceBaseAlgorithm中实现的。

ClusterManager的addItems方法就是调用的它的addItems方法,那么我们来看一下里面的addItems方法的真面目

@Override    public void addItem(T item) {        final QuadItem<T> quadItem = new QuadItem<T>(item);        synchronized (mQuadTree) {            mItems.add(quadItem);            mQuadTree.add(quadItem);        }    }

首先,再强调一点,算法类从头到尾都是在后台线程中运行的,所以对引用对象的操作都添加了线程同步操作。

这里将我们定义的MyItem对象全部转换成QuadItem对象,然后保存到QuadTree树中。这是一种四叉树,在空间

查询中很常用,有兴趣的可以从这里了解QuadTree_wiki。文章中就不详细说明。

那为什么我们要转换成QuadItem对象呢?

QuadItem内部保存着我们的MyItem对象,并且有position(经纬度)和point(点坐标)。

到这里就有必要说一下这个算法的原理了。

首先,说一个概念——世界宽度(worldWidth)

百度地图跟谷歌地图不同,它把整个地球是按照一个平面来展开,而谷歌是按照球面展开的。

把整个地球按照平面来展开之后,我们就能算出这个地球平面的宽度,也就是世界宽度。

计算公式是这样的:256*(zoom^2) == worldWidth

其中zoom就是地图的层级,我们缩放地图也是根据zoom的变化来判断是放大还是缩小的。

使用过百度地图的朋友应该知道,zoom越大,地图就放的越大,根据上面的公式我们可以知道worldWidth也会越大。

这个很好理解吧。

那说了这么多,这个worldWidth到底有什么用呢?

有了它,我们就可以把不利于计算的经纬度position转换成我们熟悉的二维坐标point了。两个point计算距离,这可就简单多了。

将position转换成point的方法如下,由

public Point toPoint(final LatLng latLng) {        final double x = latLng.longitude / 360 + .5;        final double siny = Math.sin(Math.toRadians(latLng.latitude));        final double y = 0.5 * Math.log((1 + siny) / (1 - siny)) / -(2 * Math.PI) + .5;         return new Point(x * mWorldWidth, y * mWorldWidth);    }

都是死公式,纯数学问题不多说了(丫的,高数那么久远的东西谁还记得…)

好,我们把MyItem对象转换成QuadItem了,也全都保存到QuadTree里面了,是不是要开始做正事了?

其实后面的计算就很简单了,转换一下问题就成了:一个二维坐标中有若干个点,把它们按照一个给定的distance来进行分组。

迭代所有的QuadItem,以此QuadItem为中心画一个框框,框框里面的点就算是一个cluster簇,

也就是可以聚合成一个点的QuadItems。框框的大小就是我们定义的可以执行聚合的距离。

当然啦,这话其实并不准确,会有些点同时被好多个框框给框住,那就算距离最小的那个cluster里面的点。

还是用代码来说话吧!我先强调一点,cluster就是若干个item的集合,这些item是被判定可以进行聚合的。

/**     *  cluster算法核心     * @param zoom map的级别     * @return     */    @Override    public Set<? extends Cluster<T>> getClusters(double zoom) {        final int discreteZoom = (int) zoom;         final double zoomSpecificSpan = MAX_DISTANCE_AT_ZOOM / Math.pow(2, discreteZoom) / 256;//定义的可进行聚合的距离         final Set<QuadItem<T>> visitedCandidates = new HashSet<QuadItem<T>>();  //遍历QuadItem时保存被遍历过的Item        final Set<Cluster<T>> results = new HashSet<Cluster<T>>();  //保存要返回的cluster簇,每个cluster中包含若干个MyItem对象        final Map<QuadItem<T>, Double> distanceToCluster = new HashMap<QuadItem<T>, Double>();  //Item --> 此Item与所属的cluster中心点的距离        final Map<QuadItem<T>, mapapi.clusterutil.clustering.algo.StaticCluster<T>> itemToCluster =                new HashMap<QuadItem<T>, mapapi.clusterutil.clustering.algo.StaticCluster<T>>(); //Item对象 --> 此Item所属的cluster         synchronized (mQuadTree) {            for (QuadItem<T> candidate : mItems) {<span style="white-space:pre">	</span>//遍历所有的QuadItem                if (visitedCandidates.contains(candidate)) {<span style="white-space:pre">	</span>//如果此Item已经被别的cluster框住了,就不再处理它                                     continue;                }                 Bounds searchBounds = createBoundsFromSpan(candidate.getPoint(), zoomSpecificSpan);//这个就是我们说的,根据给定距离生成一个框框                Collection<QuadItem<T>> clusterItems;                // search 某边界范围内的clusterItems                clusterItems = mQuadTree.search(searchBounds); //从quadTree中搜索出框框内的点                if (clusterItems.size() == 1) {                    // 如果只有一个点,那么这一个点就是一个cluster,QuadItem也实现了Cluster接口,也可以当作Cluster对象                    results.add(candidate);                    visitedCandidates.add(candidate);                    distanceToCluster.put(candidate, 0d);                    continue;<span style="white-space:pre">	</span>//并且结束此次循环                }                mapapi.clusterutil.clustering.algo.StaticCluster<T> cluster =                        new mapapi.clusterutil.clustering.algo                                .StaticCluster<T>(candidate.mClusterItem.getPosition());//如果搜索到多个点,那么就以此item为中心创建一个cluster                results.add(cluster);                 for (QuadItem<T> clusterItem : clusterItems) {<span style="white-space:pre">	</span>//遍历所有框住的点                    Double existingDistance = distanceToCluster.get(clusterItem);//获取此item与原来的cluster中心的距离(如果之前已经被其他cluster给框住了)                    double distance = distanceSquared(clusterItem.getPoint(), candidate.getPoint());<span style="white-space:pre">	</span>//获取此item与现在这个cluster中心的距离                    if (existingDistance != null) {                        // 判断那个距离跟小                        if (existingDistance < distance) {                            continue;                        }                        //如果跟现在的cluster距离更近,则将此item从原来的cluster中移除                        itemToCluster.get(clusterItem).remove(clusterItem.mClusterItem);                    }                    distanceToCluster.put(clusterItem, distance); //保存此item到cluster中心的距离                    cluster.add(clusterItem.mClusterItem);<span style="white-space:pre">	</span>//将此item添加到cluster中                    itemToCluster.put(clusterItem, cluster);<span style="white-space:pre">	</span>//建立item -- cluster 的map                }                visitedCandidates.addAll(clusterItems);//将所有框住过的点添加到已访问的List中。            }        }        return results;    }

返回一个cluster的集合,算法类的工作就算完成了。

那么上篇到此就结束了,下篇中将分析如何在地图上处理这些item以及实现动画,

值得一提的是,那里的绝大部分工作都是在UI线程完成的,是不是很神奇!!?

点这里可以跳转到人工智能网站