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
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
.
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)
}
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)
This tutorial shows 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)
}
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.
If you only need to change the icon or color you can add buckets with multiple colors/images when initializing your GMUDefaultClusterIconGenerator (or just one as in the case below if you only need one color). I used a large number (higher than the max number of cluster items) so that all clusters will have the same color. To use multiple colors you can add multiple buckets and multiple colors.
let iconGenerator = GMUDefaultClusterIconGenerator.init(buckets: [99999], backgroundColors: [UIColor.red])
let algorithm = GMUNonHierarchicalDistanceBasedAlgorithm()
let renderer = GMUDefaultClusterRenderer(mapView: googleMapView, clusterIconGenerator: iconGenerator)
clusterManager = GMUClusterManager(map: googleMapView, algorithm: algorithm, renderer: renderer)
clusterManager.setDelegate(self, mapDelegate: self)
To use an image as your cluster background you can provide a group of backgroundImages for your buckets:
let iconGenerator = GMUDefaultClusterIconGenerator.init(buckets: [99999], backgroundImages: [UIImage(named: "YOUR_IMAGE_HERE")!])