点聚合效果

解决用户在地图上绘制大量标注点时,出现相互压盖、底图加载的卡顿的问题。
下载源代码
00:00 / 00:12
体验移动端 扫码体验移动端

使用场景

当地图上需要展示的marker过多,可能会导致界面上marker压盖、性能变差。使用点聚合功能,则可以解决该问题。效果示例请见右侧视频。

用到产品

iOS 地图 SDK

核心类/接口

接口

说明

版本

MAMapView

(void) addAnnotations:    (NSArray *)   annotations

向地图窗口添加一组标注,需要实现MAMapViewDelegate的-mapView:viewForAnnotation:函数来生成标注对应的View

V4.3.0版本起

核心难点

1、调用ClusterAnnotation文件夹下的代码能够实现poi点聚合,使用步骤如下:

初始化coordinateQuadTree。

self.coordinateQuadTree = [[CoordinateQuadTree alloc] init];
var coordinateQuadTree = CoordinateQuadTree()

2、获得poi数组pois后,创建coordinateQuadTree。

·  项目Demo通过关键字搜索获得poi数组数据,具体见工程。此处从获得poi数组开始说明。

·  创建四叉树coordinateQuadTree来建立poi的四叉树索引。

·  创建过程较为费时,建议另开线程。创建四叉树完成后,计算当前mapView下需要显示的annotation。

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    /* 建立四叉树. */
    [self.coordinateQuadTree buildTreeWithPOIs:respons.pois];

    dispatch_async(dispatch_get_main_queue(), ^{
            /* 计算当前mapView区域内需要显示的annotation. */
            NSLog(@"First time calculate annotations.");
            [self addAnnotationsToMapView:self.mapView];
    });
});
DispatchQueue.global(qos: .default).async(execute: { [weak self] in

                self?.coordinateQuadTree.build(withPOIs: response.pois)
                self?.shouldRegionChangeReCalculate = true
                self?.addAnnotations(toMapView: (self?.mapView)!)
            })

3、根据CoordinateQuadTree四叉树索引,计算当前zoomLevel下,mapView区域内的annotation。

- (void)addAnnotationsToMapView:(MAMapView *)mapView
{
    /* 判断是否已建树. */
    if (self.coordinateQuadTree.root == nil)
    {
        return;
    }
    /* 根据当前zoomLevel和zoomScale 进行annotation聚合. */
    double zoomScale = self.mapView.bounds.size.width / self.mapView.visibleMapRect.size.width;
    /* 基于先前建立的四叉树索引,计算当前需要显示的annotations. */
    NSArray *annotations = [self.coordinateQuadTree clusteredAnnotationsWithinMapRect:mapView.visibleMapRect
                                withZoomScale:zoomScale
                                 andZoomLevel:mapView.zoomLevel];

    /* 更新annotations. */
    [self updateMapViewAnnotationsWithAnnotations:annotations];
}
func addAnnotations(toMapView mapView: MAMapView) {
    synchronized(lock: self) { [weak self] in

        guard (self?.coordinateQuadTree.root != nil) || self?.shouldRegionChangeReCalculate != false else {
            NSLog("tree is not ready.")
            return
        }

        guard let aMapView = self?.mapView else {
            return
        }

        let visibleRect = aMapView.visibleMapRect
        let zoomScale = Double(aMapView.bounds.size.width) / visibleRect.size.width
        let zoomLevel = Double(aMapView.zoomLevel)

        DispatchQueue.global(qos: .default).async(execute: { [weak self] in

            let annotations = self?.coordinateQuadTree.clusteredAnnotations(within: visibleRect, withZoomScale: zoomScale, andZoomLevel: zoomLevel)

            self?.updateMapViewAnnotations(annotations: annotations as! Array<ClusterAnnotation>)
        })
    }
}

4、更新annotations。对比mapView里已有的annotations,吐故纳新。

/* 更新annotation. */
- (void)updateMapViewAnnotationsWithAnnotations:(NSArray *)annotations
{
    /* 用户滑动时,保留仍然可用的标注,去除屏幕外标注,添加新增区域的标注 */
    NSMutableSet *before = [NSMutableSet setWithArray:self.mapView.annotations];
    [before removeObject:[self.mapView userLocation]];
    NSSet *after = [NSSet setWithArray:annotations];

    /* 保留仍然位于屏幕内的annotation. */
    NSMutableSet *toKeep = [NSMutableSet setWithSet:before];
    [toKeep intersectSet:after];

    /* 需要添加的annotation. */
    NSMutableSet *toAdd = [NSMutableSet setWithSet:after];
    [toAdd minusSet:toKeep];

    /* 删除位于屏幕外的annotation. */
    NSMutableSet *toRemove = [NSMutableSet setWithSet:before];
    [toRemove minusSet:after];

    /* 更新. */
    dispatch_async(dispatch_get_main_queue(), ^{
        [self.mapView addAnnotations:[toAdd allObjects]];
        [self.mapView removeAnnotations:[toRemove allObjects]];
    });
}
func updateMapViewAnnotations(annotations: Array<ClusterAnnotation>) {

        /* 用户滑动时,保留仍然可用的标注,去除屏幕外标注,添加新增区域的标注 */
        let before = NSMutableSet(array: mapView.annotations)
        before.remove(mapView.userLocation)
        let after: Set<NSObject> = NSSet(array: annotations) as Set<NSObject>

        /* 保留仍然位于屏幕内的annotation. */
        var toKeep: Set<NSObject> = NSMutableSet(set: before) as Set<NSObject>
        toKeep = toKeep.intersection(after)

        /* 需要添加的annotation. */
        let toAdd = NSMutableSet(set: after)
        toAdd.minus(toKeep)

        /* 删除位于屏幕外的annotation. */
        let toRemove = NSMutableSet(set: before)
        toRemove.minus(after)

        DispatchQueue.main.async(execute: { [weak self] () -> Void in
            self?.mapView.addAnnotations(toAdd.allObjects)
            self?.mapView.removeAnnotations(toRemove.allObjects)
        })
    }
架构

Controllers  BaseMapViewController //地图基类 AnnotationClusterViewController //poi点聚合 PoiDetailViewController //显示poi详细信息列表

View

MAAnnotationView
    ClusterAnnotationView //自定义的聚合annotationView

Models

Conform to <MAAnnotation>
    ClusterAnnotation //记录annotation的信息,如其代表的poi数组、poi的个数、poi平均坐标,并提供两个annotation是否Equal的判断
CoordinateQuadTree //封装的四叉树类
QuadTree //四叉树基本算法
下载源代码
00:00 / 00:12
体验移动端 扫码体验移动端

使用场景

当地图上需要展示的marker过多,可能会导致界面上marker压盖、性能变差。使用点聚合功能,则可以解决该问题。

在之前的开源工程版本基础上做了一些修改,提高了显示速度和内存优化使用,支持的聚合点更多,效果更好。主要修改包括:

1、将marker处理线程和聚合算法线程进行分离,提高效率。

2、聚合过程中减少不必要的运算,提高聚合显示速度。

3、适配新版SDK应用场景,对用到的图标,统一管理,减少内存使用和提高效。

4、加入了动画效果,聚合切换过程更为平滑。

用到产品

Android 地图 SDK

核心类/接口

接口

说明

版本

AMap

setOnCameraChangeListener(this);

定义了当可视范围改变时回调的接口。

V2.0.0版本起

AMap

setOnMarkerClickListener(this)

定义了当marker 对象被点击时回调的接口

V2.0.0版本起

AMap

getScalePerPixel()

获取比例尺数据。当前缩放级别下,地图上1像素点对应的长度,单位米。

V2.0.0版本起

AMap

getProjection()

返回一个Projection对象。可以通过这个对象在屏幕坐标与经纬度坐标之间进行转换。Projection对象返回的是当前可视区域的坐标,当可视区域变换时,它不会自己更新。

V2.0.0版本起

核心难点

1、初始化聚合和加入元素

批量初始化,适合一次大量加入

List<ClusterItem> items = new ArrayList<ClusterItem>();
 //随机10000个点
 for (int i = 0; i < 10000; i++) {
     double lat = Math.random() + 39.474923;
     double lon = Math.random() + 116.027116;
     LatLng latLng = new LatLng(lat, lon, false);
     RegionItem regionItem = new RegionItem(latLng,
             "test" + i);
     items.add(regionItem);

 }
mClusterOverlay = new ClusterOverlay(mAMap, items,
         dp2px(getApplicationContext(), clusterRadius),
               getApplicationContext());
               

初始化之后,有新的聚合点加入

double lat = Math.random() + 39.474923;
double lon = Math.random() + 116.027116;

LatLng latLng1 = new LatLng(lat, lon, false);
RegionItem regionItem = new RegionItem(latLng1,
        "test");
mClusterOverlay.addClusterItem(regionItem);

2、设置渲染render和聚合点点击事件监听

mClusterOverlay.setClusterRenderer(MainActivity.this);
mClusterOverlay.setOnClusterClickListener(MainActivity.this);

3、自定义渲染

public Drawable getDrawAble(int clusterNum) {
    int radius = dp2px(getApplicationContext(), 80);
    if (clusterNum == 1) {
        Drawable bitmapDrawable = mBackDrawAbles.get(1);
        if (bitmapDrawable == null) {
            bitmapDrawable =
                    getApplication().getResources().getDrawable(
                    R.drawable.icon_openmap_mark);
            mBackDrawAbles.put(1, bitmapDrawable);
        }

        return bitmapDrawable;
    } else if (clusterNum < 5) {

        Drawable bitmapDrawable = mBackDrawAbles.get(2);
        if (bitmapDrawable == null) {
            bitmapDrawable = new BitmapDrawable(null, drawCircle(radius,
                    Color.argb(159, 210, 154, 6)));
            mBackDrawAbles.put(2, bitmapDrawable);
        }

        return bitmapDrawable;
    } else if (clusterNum < 10) {
        Drawable bitmapDrawable = mBackDrawAbles.get(3);
        if (bitmapDrawable == null) {
            bitmapDrawable = new BitmapDrawable(null, drawCircle(radius,
                    Color.argb(199, 217, 114, 0)));
            mBackDrawAbles.put(3, bitmapDrawable);
        }

        return bitmapDrawable;
    } else {
        Drawable bitmapDrawable = mBackDrawAbles.get(4);
        if (bitmapDrawable == null) {
            bitmapDrawable = new BitmapDrawable(null, drawCircle(radius,
                    Color.argb(235, 215, 66, 2)));
            mBackDrawAbles.put(4, bitmapDrawable);
        }

        return bitmapDrawable;
     }
}

4、聚合点击事件

public void onClick(Marker marker, List<ClusterItem> clusterItems) {
    mClusterItems = clusterItems;
    mCenterLat = marker.getPosition();
    LatLngBounds.Builder builder = new LatLngBounds.Builder();
    for (ClusterItem clusterItem : clusterItems) {
        builder.include(clusterItem.getPosition());


    }
    LatLngBounds latLngBounds = builder.build();
    mAMap.animateCamera(CameraUpdateFactory.newLatLngBounds(latLngBounds, 0)
    );
}

 

返回顶部 示例中心 常见问题 智能客服 公众号
二维码