Skip to content

Instantly share code, notes, and snippets.

@KoCMoHaBTa
Created June 5, 2020 13:08
Show Gist options
  • Save KoCMoHaBTa/d92ea94515fcfdb11e975f9bf63b09f1 to your computer and use it in GitHub Desktop.
Save KoCMoHaBTa/d92ea94515fcfdb11e975f9bf63b09f1 to your computer and use it in GitHub Desktop.
//
// GMSMapView+ClusterKit.swift
// ClusterKit
//
// Created by Milen Halachev on 5.06.20.
// Copyright © 2020 Elders. All rights reserved.
//
import Foundation
import GoogleMaps
import ClusterKit
/**
The GMSMapViewDataSource protocol is adopted by an object that mediates the GMSMapView’™s data. The data source provides the markers that represent clusters on map.
*/
@objc protocol GMSMapViewDataSource: NSObjectProtocol {
/**
Asks the data source for a marker that represent the given cluster.
@param mapView A map view object requesting the marker.
@param cluster The cluster to represent.
@return An object inheriting from GMSMarker that the map view can use for the specified cluster.
*/
@objc optional func mapView(_ mapView: GMSMapView, markerFor cluster: CKCluster) -> GMSMarker
}
/**
GMSMarker category adopting the CKAnnotation protocol.
*/
extension GMSMarker {
/**
The cluster that the marker is related to.
*/
private static var clusterKey = ""
weak var cluster: CKCluster? {
get { objc_getAssociatedObject(self, &Self.clusterKey) as? CKCluster }
set { objc_setAssociatedObject(self, &Self.clusterKey, newValue, .OBJC_ASSOCIATION_ASSIGN) }
}
}
/**
GMSMapView category adopting the CKMap protocol.
*/
extension GMSMapView: CKMap {
private static var clusterManagerKey = ""
public var clusterManager: CKClusterManager {
var clusterManager = objc_getAssociatedObject(self, &Self.clusterManagerKey) as? CKClusterManager
if clusterManager == nil {
clusterManager = .init()
clusterManager?.map = self
objc_setAssociatedObject(self, &Self.clusterManagerKey, clusterManager, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
return clusterManager!
}
private static var dataSourceKey = ""
/**
Data source instance that adopt the GMSMapViewDataSource.
*/
weak var dataSource: GMSMapViewDataSource? {
get { objc_getAssociatedObject(self, &Self.dataSourceKey) as? GMSMapViewDataSource }
set { objc_setAssociatedObject(self, &Self.dataSourceKey, newValue, .OBJC_ASSOCIATION_ASSIGN) }
}
private static var markersKey = ""
private var markers: NSMapTable<CKCluster, GMSMarker> {
var markers = objc_getAssociatedObject(self, &Self.markersKey) as? NSMapTable<CKCluster, GMSMarker>
if markers == nil {
markers = .strongToStrongObjects()
objc_setAssociatedObject(self, &Self.markersKey, markers, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
return markers!
}
/**
Returns the marker representing the given cluster.
@param cluster The cluster for which to return the corresponding marker.
@return The value associated with cluster, or nil if no value is associated with cluster.
*/
func marker(for cluster: CKCluster) -> GMSMarker? {
return self.markers.object(forKey: cluster)
}
public var visibleMapRect: MKMapRect {
let bounds = GMSCoordinateBounds(region: self.projection.visibleRegion())
let sw = MKMapPoint(bounds.southWest)
let ne = MKMapPoint(bounds.northEast);
let x = sw.x
let y = ne.y
var width = ne.x - sw.x
var height = sw.y - ne.y
// Handle antimeridian crossing
if width < 0 {
width = ne.x + MKMapSize.world.width - sw.x
}
if height < 0 {
height = sw.y + MKMapSize.world.height - ne.y
}
return .init(x: x, y: y, width: width, height: height)
}
public var zoom: Double {
let bounds = GMSCoordinateBounds(region: self.projection.visibleRegion())
var longitudeDelta = bounds.northEast.longitude - bounds.southWest.longitude
// Handle antimeridian crossing
if longitudeDelta < 0 {
longitudeDelta = 360 + bounds.northEast.longitude - bounds.southWest.longitude
}
return log2(360 * Double(self.frame.size.width) / (256 * longitudeDelta))
}
private func add(_ cluster: CKCluster) {
var marker = self.dataSource?.mapView?(self, markerFor: cluster)
if marker == nil {
marker = .init(position: cluster.coordinate)
if cluster.count > 1 {
marker?.icon = GMSMarker.markerImage(with: .purple)
}
}
marker?.cluster = cluster;
marker?.zIndex = 1;
marker?.map = self;
self.markers.setObject(marker, forKey: cluster)
}
private func remove(_ cluster: CKCluster) {
let marker = self.markers.object(forKey: cluster)
marker?.map = nil
self.markers.removeObject(forKey: cluster)
}
public func add(_ clusters: [CKCluster]) {
clusters.forEach({ self.add($0) })
}
public func remove(_ clusters: [CKCluster]) {
clusters.forEach({ self.remove($0) })
}
public func perform(_ animations: [CKClusterAnimation], completion: ((Bool) -> Void)? = nil) {
var animationsBlock: () -> Void = {}
var completionBlock: (Bool) -> Void = { finished in
completion?(finished)
}
for animation in animations {
let marker = self.markers.object(forKey: animation.cluster)
marker?.zIndex = 0
marker?.position = animation.from
let previousAnimationsBlock = animationsBlock
animationsBlock = {
previousAnimationsBlock()
marker?.layer.latitude = animation.to.latitude;
marker?.layer.longitude = animation.to.longitude;
}
let previousCompletionBlock = completionBlock
completionBlock = { finished in
marker?.zIndex = 1
previousCompletionBlock(finished)
}
}
if self.clusterManager.delegate?.clusterManager?(self.clusterManager, performAnimations: animationsBlock, completion: completionBlock) == nil {
let curve: CAMediaTimingFunction
switch (self.clusterManager.animationOptions) {
case .curveEaseInOut:
curve = .init(name: .easeInEaseOut)
break;
case .curveEaseIn:
curve = .init(name: .easeIn)
break;
case .curveEaseOut:
curve = .init(name: .easeOut)
break;
case .curveLinear:
curve = .init(name: .linear)
break;
default:
curve = .init(name: .default)
}
CATransaction.begin()
CATransaction.setAnimationDuration(CFTimeInterval(self.clusterManager.animationDuration))
CATransaction.setAnimationTimingFunction(curve)
CATransaction.setCompletionBlock {
completionBlock(true)
}
animationsBlock()
CATransaction.commit()
}
}
public func select(_ cluster: CKCluster, animated: Bool) {
let marker = self.markers.object(forKey: cluster)
if marker !== self.selectedMarker {
marker?.map = self
self.selectedMarker = marker
}
}
public func deselect(_ cluster: CKCluster, animated: Bool) {
let marker = self.markers.object(forKey: cluster)
if marker === self.selectedMarker {
self.selectedMarker = nil
}
}
}
/**
GMSCameraUpdate for modifying the camera to show the content of a cluster.
*/
extension GMSCameraUpdate {
/**
Returns a GMSCameraUpdate that transforms the camera such that the specified cluster are centered on screen at the greatest possible zoom level. The bounds will have a default padding of 64 points.
The returned camera update will set the camera's bearing and tilt to their default zero values (i.e., facing north and looking directly at the Earth).
@param cluster The cluster to fit.
@return The camera update that fit the given cluster.
*/
static func fit(_ cluster: CKCluster) -> GMSCameraUpdate {
return self.fit(cluster, withPadding: 64)
}
/**
This is similar to fitCluster: but allows specifying the padding (in points) in order to inset the bounding box from the view's edges.
@param cluster The cluster to fit.
@param padding The padding that inset the bounding box. If the requested padding is larger than the view size in either the vertical or horizontal direction the map will be maximally zoomed out.
@return The camera update that fit the given cluster.
*/
static func fit(_ cluster: CKCluster, withPadding padding: CGFloat) -> GMSCameraUpdate {
let edgeInsets = UIEdgeInsets(top: padding, left: padding, bottom: padding, right: padding)
return self.fit(cluster, with: edgeInsets)
}
/**
This is similar to fitCluster: but allows specifying edge insets in order to inset the bounding box from the view's edges.
@param cluster The cluster to fit.
@param edgeInsets The edge insets of the bounding box. If the requested edge insets are larger than the view size in either the vertical or horizontal direction the map will be maximally zoomed out.
@return The camera update that fit the given cluster.
*/
static func fit(_ cluster: CKCluster, with edgeInsets: UIEdgeInsets) -> GMSCameraUpdate {
let bounds = GMSCoordinateBounds(coordinate: cluster.coordinate, coordinate: cluster.coordinate)
for i in 0..<cluster.count {
let marker = cluster[i]
bounds.includingCoordinate(marker.coordinate)
}
return self.fit(bounds, with: edgeInsets)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment