How to implement GMUClusterRenderer in Swift

10,440

Solution 1

I found a clean solution for clustered markers, on Swift 4, to use a custom image for the cluster with the number of cluster inside:

class MapClusterIconGenerator: GMUDefaultClusterIconGenerator {

    override func icon(forSize size: UInt) -> UIImage {
        let image = textToImage(drawText: String(size) as NSString,
                                inImage: UIImage(named: "cluster")!,
                                font: UIFont.systemFont(ofSize: 12))
        return image
    }

    private func textToImage(drawText text: NSString, inImage image: UIImage, font: UIFont) -> UIImage {

        UIGraphicsBeginImageContext(image.size)
        image.draw(in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height))

        let textStyle = NSMutableParagraphStyle()
        textStyle.alignment = NSTextAlignment.center
        let textColor = UIColor.black
        let attributes=[
            NSAttributedStringKey.font: font,
            NSAttributedStringKey.paragraphStyle: textStyle,
            NSAttributedStringKey.foregroundColor: textColor]

        // vertically center (depending on font)
        let textH = font.lineHeight
        let textY = (image.size.height-textH)/2
        let textRect = CGRect(x: 0, y: textY, width: image.size.width, height: textH)
        text.draw(in: textRect.integral, withAttributes: attributes)
        let result = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return result!
    }

}

Than the setup for the cluster manager:

private func setupClustering() {
    guard let mapView = self.mapView else { return }

    let iconGenerator = MapClusterIconGenerator()
    let renderer = MapClusterRenderer(mapView: mapView, clusterIconGenerator: iconGenerator)
    let algorithm = GMUNonHierarchicalDistanceBasedAlgorithm()
    clusterManager = GMUClusterManager(map: mapView, algorithm: algorithm, renderer: renderer)
}

I also used a custom cluster renderer MapClusterRenderer.

Solution 2

I manage to find "clean" solution, even though it's still confusing. But it works!

1) Create .h file "MarkerManager"

    #import <Foundation/Foundation.h>
@import CoreLocation;
#import "GMUClusterItem.h"
#import <GoogleMaps/GoogleMaps.h>


@interface MarkerManager: NSObject

@property (nonatomic) CLLocationCoordinate2D location;
@property (nonatomic, strong) GMSMarker *marker;

@end

2) Go to GMUDefaultClusterRenderer class in Google-Maps-iOS-Utils folder, import MarkerManager.h class find and change this method:

// Returns a marker at final position of |position| with attached |userData|.
// If animated is YES, animates from the closest point from |points|.
- (GMSMarker *)markerWithPosition:(CLLocationCoordinate2D)position
                             from:(CLLocationCoordinate2D)from
                         userData:(id)userData
                      clusterIcon:(UIImage *)clusterIcon
                         animated:(BOOL)animated {
  GMSMarker *marker = [self markerForObject:userData];
  CLLocationCoordinate2D initialPosition = animated ? from : position;
  marker.position = initialPosition;
  marker.userData = userData;
  if (clusterIcon != nil) {
    marker.icon = clusterIcon;
    marker.groundAnchor = CGPointMake(0.5, 0.5);
  }
  //added
  else {
      MarkerManager *data = userData;
      if(data != nil) {
          marker.icon = data.marker.icon;
      }
  }
  //ends here

  marker.zIndex = _zIndex;

  if ([_delegate respondsToSelector:@selector(renderer:willRenderMarker:)]) {
    [_delegate renderer:self willRenderMarker:marker];
  }
  marker.map = _mapView;

  if (animated) {
    [CATransaction begin];
    [CATransaction setAnimationDuration:kGMUAnimationDuration];
    marker.layer.latitude = position.latitude;
    marker.layer.longitude = position.longitude;
    [CATransaction commit];
  }

  if ([_delegate respondsToSelector:@selector(renderer:didRenderMarker:)]) {
    [_delegate renderer:self didRenderMarker:marker];
  }
  return marker;
}

3) Create new swift class, POIItem:

class POIItem: NSObject, GMUClusterItem {
var position: CLLocationCoordinate2D
@objc var marker: GMSMarker!


init(position: CLLocationCoordinate2D, marker: GMSMarker) {
    self.position = position
    self.marker = marker
}
}

4) Extend GMUDefaultClusterRenderer class and override markerWithPosition method:

import Foundation
import UIKit

class CustomMarkers: GMUDefaultClusterRenderer {
    var mapView:GMSMapView?
    let kGMUAnimationDuration: Double = 0.5

    override init(mapView: GMSMapView, clusterIconGenerator iconGenerator: GMUClusterIconGenerator) {

        super.init(mapView: mapView, clusterIconGenerator: iconGenerator)
    }

    func markerWithPosition(position: CLLocationCoordinate2D, from: CLLocationCoordinate2D, userData: AnyObject, clusterIcon: UIImage, animated: Bool) -> GMSMarker {
        let initialPosition = animated ? from : position
        let marker = GMSMarker(position: initialPosition)
        marker.userData! = userData
        if clusterIcon.cgImage != nil {
            marker.icon = clusterIcon
        }
        else {
            marker.icon = self.getCustomTitleItem(userData: userData)

        }
        marker.map = mapView
        if animated
        {
            CATransaction.begin()
            CAAnimation.init().duration = kGMUAnimationDuration
            marker.layer.latitude = position.latitude
            marker.layer.longitude = position.longitude
            CATransaction.commit()
        }
        return marker
    }

    func getCustomTitleItem(userData: AnyObject) -> UIImage {
        let item = userData as! POIItem
        return item.marker.icon!
    }
}

5) In MapViewController init POIItem in generateClusterItems method:

private func generateClusterItems() {

        for object in DataManager.sharedInstance.mapItemsArray {

            let doubleLat = Double(object.latitude)
            let doubleLong = Double(object.longitude)
            let latitude = CLLocationDegrees(doubleLat!)
            let longitude = CLLocationDegrees(doubleLong!)
            let position = CLLocationCoordinate2DMake(latitude, longitude)
            let marker = GMSMarker(position: position)
            let item = POIItem(position: position, marker: marker)
            self.clusterManager.add(item)                
            item.mapItem = object

        }
    }

Inside the for loop you can call:

marker.icon = UIImage(named:"YOUR_IMAGE_NAME")

Now you can set logic to have more then one custom markers.

Solution 3

In Swift 4.2 :

you can use GMUClusterRendererDelegate:

Add this extension to your controller and be sure that your controller is the delegate of GMUClusterRendererDelegate :

willRenderMarker will call each time a marker going to be render (both cluster marker and clusterItemMarker so you can check it by simple if).so you can modify it's icon and etc before showing it to user

extension YourController: GMUClusterRendererDelegate {
    func renderer(_ renderer: GMUClusterRenderer, willRenderMarker marker: GMSMarker) {
        // if your marker is pointy you can change groundAnchor
        marker.groundAnchor = CGPoint(x: 0.5, y: 1)
        if  let markerData = (marker.userData as? PersonMarker) {
           let icon = markerData.imageURL
           marker.iconView = CustomMarkerView(forUrl: url)
        }
    }
}

And PersonMarker is your marker class that subclass NSObject and GMUClusterItem : (you can use default class of GMUClusterItem but if you need some other properties you can subclass it)

class PersonMarker: NSObject, GMUClusterItem {

  var position: CLLocationCoordinate2D
  var imageURL : String?
  var name: String?
  var userdId: String?
  var lastSeen: String?

  init(position: CLLocationCoordinate2D, url: String?, name: String?, userId: String?, lastSeen: String?) {
      self.position = position
      self.imageURL = url
      self.name = name
      self.userdId = userId
      self.lastSeen = lastSeen
  }

}

You can add PersonMarker to your GMUClusterManager like this :

 let position = CLLocationCoordinate2D(latitude: item.latitude!, longitude: item.longitute!)
 let person = PersonMarker(position: position, url: item.user?.avaterUrl, name: item.user?.name, userId: item.user?.userId, lastSeen: item.lastUpdate)
 clusterManager.add(person)

Solution 4

Here Is shows step by step instructions on how to group multiple map markers with Marker Clustering

1:

class HFDashBordVC: UIViewController,GMUClusterManagerDelegate,GMSMapViewDelegate{

  private var clusterManager: GMUClusterManager!

 var cameraPosition = GMSCameraPosition()
var markerPin = GMSMarker()
var currentLocationCircle = GMSCircle()
var locationManager = CLLocationManager()
var currentLocation: CLLocation?
var zoomLevel: Float = 18.0

@IBOutlet var mapView: GMSMapView!

override func viewDidLoad() {
    super.viewDidLoad()
    
    setupClustering()
}

 func setupClustering() {
    let iconGenerator = CustomClusterIconGenerator()
    let algorithm = GMUNonHierarchicalDistanceBasedAlgorithm()
    let renderer = CustomRendererMarkers(mapView: mapView,
                                         clusterIconGenerator: iconGenerator)
    clusterManager = GMUClusterManager(map: mapView, algorithm: algorithm,
                                       renderer: renderer)
    clusterManager.cluster()
    clusterManager.setDelegate(self, mapDelegate: self)
}

private func setClusterParkingPin() {

    clusterManager.clearItems()
    for object in AppUtilites.sharedInstance.safeParkingArray {
        
        let model = object as! AMParkModel
        let lat = Double(model.latitude)!
        let lng = Double(model.longitude)!
        let position = CLLocationCoordinate2DMake(lat, lng)
        let marker = GMSMarker(position: position)
        marker.icon = HFAsset.ic_p_pin_orange.image 
        let userData = model
        let item = POIItem(position: position, marker: marker, userData: userData)
        self.clusterManager.add(item)
        self.setCurrentPin(zoomLevel: 14.0)
        
    }
}

2 :

 class POIItem: NSObject, GMUClusterItem {
        var position: CLLocationCoordinate2D
       var userData: AnyObject!
       @objc var marker: GMSMarker!


  init(position: CLLocationCoordinate2D, marker: GMSMarker, userData: AnyObject) {
    self.position = position
    self.marker = marker
    self.userData = userData
   }
 }

3 :

  class CustomRendererMarkers: GMUDefaultClusterRenderer {
var mapView:GMSMapView?
let kGMUAnimationDuration: Double = 0.5

override init(mapView: GMSMapView, clusterIconGenerator iconGenerator: GMUClusterIconGenerator) {
    
    super.init(mapView: mapView, clusterIconGenerator: iconGenerator)
}

func markerWithPosition(position: CLLocationCoordinate2D, from: CLLocationCoordinate2D, userData: AnyObject, clusterIcon: UIImage, animated: Bool) -> GMSMarker {
    let initialPosition = animated ? from : position
    let marker = GMSMarker(position: initialPosition)
    marker.userData! = userData
    if clusterIcon.cgImage != nil {
        marker.icon = clusterIcon
    }
    else {
        marker.icon = self.getCustomTitleItem(userData: userData)
        
    }
    marker.map = mapView
    if animated
    {
        CATransaction.begin()
        CAAnimation.init().duration = kGMUAnimationDuration
        marker.layer.latitude = position.latitude
        marker.layer.longitude = position.longitude
        CATransaction.commit()
    }
    return marker
}

func getCustomTitleItem(userData: AnyObject) -> UIImage {
    let item = userData as! POIItem
    return item.marker.icon!
 }
}

4 :

class CustomClusterIconGenerator: GMUDefaultClusterIconGenerator {

override func icon(forSize size: UInt) -> UIImage {
    let image = textToImage(drawText: (String(size) as NSString) as String,
                            inImage: HFAsset.ic_parking_clustering.image,
                            font: UIFont.init(name: "Montserrat-Medium", size: 12.0)!)
    return image
}

private func textToImage(drawText text: String, inImage image: UIImage, font: UIFont) -> UIImage {
    var pinCount = text
    if Int(text)! > 9999 {
        pinCount = "+9999"
    }
    
    UIGraphicsBeginImageContext(image.size)
    image.draw(in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height))
    
    let textStyle = NSMutableParagraphStyle()
    textStyle.alignment = NSTextAlignment.center
    let textColor = UIColor.white
    let attributes=[
        NSAttributedStringKey.font: font,
        NSAttributedStringKey.paragraphStyle: textStyle,
        NSAttributedStringKey.foregroundColor: textColor,
        NSAttributedStringKey.backgroundColor: appThemeColor]
    
    // vertically center (depending on font)
    let textH = font.lineHeight
    let textY = (image.size.height-textH)/3
    let textRect = CGRect(x: 0, y: textY, width: image.size.width, height: textH)
    pinCount.draw(in: textRect.integral, withAttributes: attributes)
    let result = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    return result!
  }

 }

5 :

    func mapView(_ mapView: GMSMapView, didTap marker: GMSMarker) -> Bool {
    
    if markerPin == marker {
        return true
    }
    
    if ((marker.userData as? GMUStaticCluster) != nil) {   // Cluster Pin tap
        UIView.animate(withDuration: 0.5, delay: 0.2, options: [.curveEaseOut],
                       animations: {
                        let newCamera = GMSCameraPosition.camera(withTarget: marker.position,
                                                                 zoom: self.mapView.camera.zoom + 0.8)
                        let update = GMSCameraUpdate.setCamera(newCamera)
                        self.mapView.animate(with: update)
        }, completion: {
            finished in
        })
    }
   
    if let poiItem = marker.userData as? POIItem {   // Cluster Parking Pin tap
        let Detail = self.storyboard?.instantiateViewController(withIdentifier: "HFParkingFullDetailsVC")as! HFParkingFullDetailsVC
        Detail.parkModel = (poiItem.userData as? AMParkModel)!
        self.navigationController?.pushViewController(Detail, animated: true)
    }

Solution 5

If you included Google-Maps-iOS-Utils source files to your project there are one "dirty" way to change icon of marker.

Unfortunately there are no public methods to set custom icon, but you can change it in source file.

In Google Map Utils/Clustering/View/GMUDefaultClusterRenderer.m

 - (void)renderCluster:(id<GMUCluster>)cluster animated:(BOOL)animated {
 ...

      GMSMarker *marker = [self markerWithPosition:item.position
                                              from:fromPosition
                                          userData:item
                                       clusterIcon:[UIImage imageNamed:@"YOUR_CUSTOM_ICON"]
                                          animated:shouldAnimate];
 ...

}

Than you could setup your cluster manager (Swift)

 private func setupClusterManager() {
        let iconGenerator = GMUDefaultClusterIconGenerator()
        let algorithm = GMUNonHierarchicalDistanceBasedAlgorithm()
        let renderer = GMUDefaultClusterRenderer(mapView: mapView,
                                                 clusterIconGenerator: iconGenerator)


        clusterManager = GMUClusterManager(map: mapView, algorithm: algorithm,
                                           renderer: renderer)

}
Share:
10,440
S. Park
Author by

S. Park

Updated on June 16, 2022

Comments

  • S. Park
    S. Park almost 2 years

    I am using Google Maps API for iOS and want to use marker clustering utility. I figured out how to show clustered markers, but I would like to customize markers. Can someone explain how to set/change icon and title of each marker or clustered markers? An example code would be very helpful.

    class POIItem: NSObject, GMUClusterItem {
        var position: CLLocationCoordinate2D
        var name: String!
    
        init(position: CLLocationCoordinate2D, name: String) {
            self.position = position
            self.name = name
        }
    }
    
    class MyRenderer: NSObject, GMUClusterRenderer {
        var mapView: GMSMapView
        var clusterIconGenerator: GMUClusterIconGenerator
        var clusterManager: GMUClusterManager
    
        init(mapView: GMSMapView, clusterIconGenerator: GMUClusterIconGenerator, clusterManager: GMUClusterManager) {
            self.mapView = mapView
            self.clusterIconGenerator = clusterIconGenerator
            self.clusterManager = clusterManager
        }
    
        func renderClusters(clusters: [GMUCluster]) {
    
        }
    
        func update() {
    
        }
    }
    

    This is what I have so far. I don't know what to do with renderClusters and update functions.

  • bradford gray
    bradford gray over 6 years
    I like your solution and it works for Swift 3, but now in Swift 4 you get an "implicit Objective-C entrypoint, add explicit '@objc' to the declaration to emit the Objective-C entrypoint in Swift 4". Have you found another workaround?
  • nja
    nja over 6 years
    In POIItem class just add @objc in front of "var marker", I edited solution. Sorry for late response.
  • bradford gray
    bradford gray over 6 years
    Thank you for the response.
  • Nike Kov
    Nike Kov almost 5 years
  • Aleksandrs Muravjovs
    Aleksandrs Muravjovs almost 5 years
    Thanks, it works!!! The only thing which is not working by some reason is that when I tap on cluster it crashed instead of zooming in.